From 8e0811796cb10a2b387ec55f5cdc212ae6c70b98 Mon Sep 17 00:00:00 2001 From: John Wesley <47087725+jwr1@users.noreply.github.com> Date: Mon, 6 May 2024 16:10:13 -0400 Subject: [PATCH] Add post apperance options along with layout presets, mark NSFW and OC posts (#41) * Initial work on compact mode, mark NSFW and OC posts * Change content title style * Split compact mode into separate settings * Add limit title preview option * Update preset snackbar text --- lib/src/models/comment.dart | 3 - lib/src/models/post.dart | 20 +- lib/src/screens/explore/magazines_screen.dart | 2 +- lib/src/screens/feed/feed_screen.dart | 65 +- lib/src/screens/feed/post_comment.dart | 9 +- lib/src/screens/feed/post_item.dart | 7 +- lib/src/screens/feed/post_page.dart | 6 +- lib/src/screens/profile/message_item.dart | 2 +- .../screens/profile/profile_edit_screen.dart | 641 +++++++++--------- .../screens/settings/general_settings.dart | 98 ++- .../screens/settings/settings_controller.dart | 241 ++++--- lib/src/screens/settings/settings_screen.dart | 25 + lib/src/widgets/blur.dart | 48 ++ lib/src/widgets/content_item.dart | 600 +++++++++------- 14 files changed, 1032 insertions(+), 735 deletions(-) create mode 100644 lib/src/widgets/blur.dart diff --git a/lib/src/models/comment.dart b/lib/src/models/comment.dart index c23637c..ae4521f 100644 --- a/lib/src/models/comment.dart +++ b/lib/src/models/comment.dart @@ -54,7 +54,6 @@ class CommentModel with _$CommentModel { required int? boosts, required int? myVote, required bool? myBoost, - required bool? isAdult, required DateTime createdAt, required DateTime? editedAt, required List? children, @@ -82,7 +81,6 @@ class CommentModel with _$CommentModel { ? 1 : ((json['userVote'] as int?) == -1 ? -1 : 0), myBoost: (json['userVote'] as int?) == 1, - isAdult: json['isAdult'] as bool, createdAt: DateTime.parse(json['createdAt'] as String), editedAt: optionalDateTime(json['editedAt'] as String?), children: (json['children'] as List) @@ -136,7 +134,6 @@ class CommentModel with _$CommentModel { boosts: null, myVote: json['my_vote'] as int?, myBoost: null, - isAdult: null, createdAt: DateTime.parse(lemmyComment['published'] as String), editedAt: optionalDateTime(json['updated'] as String?), children: children, diff --git a/lib/src/models/post.dart b/lib/src/models/post.dart index 475d085..9b4ba50 100644 --- a/lib/src/models/post.dart +++ b/lib/src/models/post.dart @@ -61,8 +61,8 @@ class PostModel with _$PostModel { required int? boosts, required int? myVote, required bool? myBoost, - required bool? isOc, - required bool isAdult, + required bool? isOC, + required bool isNSFW, required bool isPinned, required DateTime createdAt, required DateTime? editedAt, @@ -90,8 +90,8 @@ class PostModel with _$PostModel { ? 1 : ((json['userVote'] as int?) == -1 ? -1 : 0), myBoost: (json['userVote'] as int?) == 1, - isOc: json['isOc'] as bool, - isAdult: json['isAdult'] as bool, + isOC: json['isOc'] as bool, + isNSFW: json['isAdult'] as bool, isPinned: json['isPinned'] as bool, createdAt: DateTime.parse(json['createdAt'] as String), editedAt: optionalDateTime(json['editedAt'] as String?), @@ -119,8 +119,8 @@ class PostModel with _$PostModel { ? 1 : ((json['userVote'] as int?) == -1 ? -1 : 0), myBoost: (json['userVote'] as int?) == 1, - isOc: null, - isAdult: json['isAdult'] as bool, + isOC: null, + isNSFW: json['isAdult'] as bool, isPinned: json['isPinned'] as bool, createdAt: DateTime.parse(json['createdAt'] as String), editedAt: optionalDateTime(json['editedAt'] as String?), @@ -150,10 +150,10 @@ class PostModel with _$PostModel { boosts: null, myVote: json['my_vote'] as int?, myBoost: null, - isOc: null, - isAdult: lemmyPost['nsfw'] as bool, - isPinned: (lemmyPost['featured_community'] as bool || - lemmyPost['featured_local'] as bool), + isOC: null, + isNSFW: lemmyPost['nsfw'] as bool, + isPinned: lemmyPost['featured_community'] as bool || + lemmyPost['featured_local'] as bool, createdAt: DateTime.parse(lemmyPost['published'] as String), editedAt: optionalDateTime(lemmyPost['updated'] as String?), lastActive: DateTime.parse(lemmyCounts['newest_comment_time'] as String), diff --git a/lib/src/screens/explore/magazines_screen.dart b/lib/src/screens/explore/magazines_screen.dart index 57c7f0c..9bda356 100644 --- a/lib/src/screens/explore/magazines_screen.dart +++ b/lib/src/screens/explore/magazines_screen.dart @@ -113,7 +113,7 @@ class _MagazinesScreenState extends State { ], ), ), - ...(context.read().serverSoftware == + ...(context.watch().serverSoftware == ServerSoftware.lemmy || filter == APIMagazinesFilter.all || filter == APIMagazinesFilter.local diff --git a/lib/src/screens/feed/feed_screen.dart b/lib/src/screens/feed/feed_screen.dart index 2867dcf..ed4d9c4 100644 --- a/lib/src/screens/feed/feed_screen.dart +++ b/lib/src/screens/feed/feed_screen.dart @@ -71,8 +71,8 @@ class _FeedScreenState extends State { final actions = [ feedActionCreatePost.withProps( - context.read().isLoggedIn - ? context.read().feedActionCreatePost + context.watch().isLoggedIn + ? context.watch().feedActionCreatePost : ActionLocation.hide, () async { await Navigator.of(context).push( @@ -92,7 +92,7 @@ class _FeedScreenState extends State { : parseEnum( ActionLocation.values, ActionLocation.hide, - context.read().feedActionSetFilter.name, + context.watch().feedActionSetFilter.name, ), () async { final newFilter = @@ -109,7 +109,7 @@ class _FeedScreenState extends State { parseEnum( ActionLocation.values, ActionLocation.hide, - context.read().feedActionSetSort.name, + context.watch().feedActionSetSort.name, ), () async { final newSort = await feedSortSelect.askSelection(context, _sort); @@ -123,13 +123,13 @@ class _FeedScreenState extends State { ), feedActionSetType.withProps( widget.source == FeedSource.domain && - context.read().serverSoftware == + context.watch().serverSoftware == ServerSoftware.lemmy ? ActionLocation.hide : parseEnum( ActionLocation.values, ActionLocation.hide, - context.read().feedActionSetType.name, + context.watch().feedActionSetType.name, ), () async { final newMode = await feedTypeSelect.askSelection(context, _mode); @@ -149,7 +149,7 @@ class _FeedScreenState extends State { }, ), feedActionRefresh.withProps( - context.read().feedActionRefresh, + context.watch().feedActionRefresh, () { for (var key in _feedKeyList) { key.currentState?.refresh(); @@ -157,7 +157,7 @@ class _FeedScreenState extends State { }, ), feedActionBackToTop.withProps( - context.read().feedActionBackToTop, + context.watch().feedActionBackToTop, () { for (var key in _feedKeyList) { key.currentState?.backToTop(); @@ -165,7 +165,7 @@ class _FeedScreenState extends State { }, ), feedActionExpandFab.withProps( - context.read().feedActionExpandFab, + context.watch().feedActionExpandFab, () { _fabKey.currentState?.toggle(); }, @@ -173,12 +173,12 @@ class _FeedScreenState extends State { ]; final tabsAction = [ - if (context.read().feedActionSetFilter == + if (context.watch().feedActionSetFilter == ActionLocationWithTabs.tabs && widget.source == null && - context.read().isLoggedIn) + context.watch().isLoggedIn) actions.firstWhere((action) => action.name == feedActionSetFilter.name), - if (context.read().feedActionSetType == + if (context.watch().feedActionSetType == ActionLocationWithTabs.tabs) actions.firstWhere((action) => action.name == feedActionSetType.name), ].firstOrNull; @@ -194,9 +194,9 @@ class _FeedScreenState extends State { .entries .firstWhere((entry) => entry.value.value == - (context.read().serverSoftware != + (context.watch().serverSoftware != ServerSoftware.lemmy - ? context.read().defaultFeedType + ? context.watch().defaultFeedType : PostType.thread)) .key, _ => 0 @@ -215,8 +215,8 @@ class _FeedScreenState extends State { contentPadding: EdgeInsets.zero, title: Text( widget.title ?? - context.read().selectedAccount + - (context.read().isLoggedIn + context.watch().selectedAccount + + (context.watch().isLoggedIn ? '' : ' (Guest)'), maxLines: 1, @@ -534,7 +534,12 @@ class _FeedScreenBodyState extends State { @override void didUpdateWidget(covariant FeedScreenBody oldWidget) { super.didUpdateWidget(oldWidget); - _pagingController.refresh(); + if (widget.mode != oldWidget.mode || + widget.sort != oldWidget.sort || + widget.source != oldWidget.source || + widget.sourceId != oldWidget.sourceId) { + _pagingController.refresh(); + } } @override @@ -553,10 +558,8 @@ class _FeedScreenBodyState extends State { PagedSliverList( pagingController: _pagingController, builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) => Card( - margin: const EdgeInsets.all(12), - clipBehavior: Clip.antiAlias, - child: InkWell( + itemBuilder: (context, item, index) { + final inner = InkWell( onTap: () { Navigator.of(context).push( MaterialPageRoute( @@ -584,8 +587,24 @@ class _FeedScreenBodyState extends State { }, isPreview: item.type == PostType.thread, ), - ), - ), + ); + + return context.watch().postUseCardPreview + ? Card( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + clipBehavior: Clip.antiAlias, + child: inner, + ) + : Column( + children: [ + inner, + const Divider(height: 1), + ], + ); + }, ), ) ], diff --git a/lib/src/screens/feed/post_comment.dart b/lib/src/screens/feed/post_comment.dart index 3c73218..305b4a5 100644 --- a/lib/src/screens/feed/post_comment.dart +++ b/lib/src/screens/feed/post_comment.dart @@ -44,6 +44,7 @@ class _EntryCommentState extends State { originInstance: getNameHost(context, widget.comment.user.name), body: widget.comment.body ?? '_comment deleted_', createdAt: widget.comment.createdAt, + editedAt: widget.comment.editedAt, user: widget.comment.user.name, userIcon: widget.comment.user.avatar, userIdOnClick: widget.comment.user.id, @@ -155,8 +156,8 @@ class _EntryCommentState extends State { }) : null, openLinkUri: Uri.https( - context.read().instanceHost, - context.read().serverSoftware == + context.watch().instanceHost, + context.watch().serverSoftware == ServerSoftware.lemmy ? '/comment/${widget.comment.id}' : '/m/${widget.comment.magazine.name}/${switch (widget.comment.postType) { @@ -186,12 +187,12 @@ class _EntryCommentState extends State { if (widget.comment.childCount > 0 && !_isCollapsed) Container( margin: const EdgeInsets.only(left: 1), - padding: const EdgeInsets.only(left: 10), + padding: const EdgeInsets.only(left: 9), decoration: BoxDecoration( border: Border( left: BorderSide( color: Theme.of(context).colorScheme.outlineVariant, - width: 1, + width: 2, ), ), ), diff --git a/lib/src/screens/feed/post_item.dart b/lib/src/screens/feed/post_item.dart index 02d1e15..6132827 100644 --- a/lib/src/screens/feed/post_item.dart +++ b/lib/src/screens/feed/post_item.dart @@ -36,8 +36,11 @@ class PostItem extends StatelessWidget { video: isVideo ? Uri.parse(item.url!) : null, body: item.body, createdAt: item.createdAt, + editedAt: item.editedAt, isPreview: isPreview, showMagazineFirst: item.type == PostType.thread, + isNSFW: item.isNSFW, + isOC: item.isOC == true, user: item.user.name, userIcon: item.user.avatar, userIdOnClick: item.user.id, @@ -111,8 +114,8 @@ class PostItem extends StatelessWidget { onDelete: onDelete, numComments: item.numComments, openLinkUri: Uri.https( - context.read().instanceHost, - context.read().serverSoftware == + context.watch().instanceHost, + context.watch().serverSoftware == ServerSoftware.lemmy ? '/post/${item.id}' : '/m/${item.magazine.name}/${switch (item.type) { diff --git a/lib/src/screens/feed/post_page.dart b/lib/src/screens/feed/post_page.dart index bf7c043..7074882 100644 --- a/lib/src/screens/feed/post_page.dart +++ b/lib/src/screens/feed/post_page.dart @@ -186,10 +186,10 @@ class _PostPageState extends State { .edit( post.id, post.title!, - post.isOc!, + post.isOC!, body, post.lang!, - post.isAdult, + post.isNSFW, ), PostType.microblog => context .read() @@ -199,7 +199,7 @@ class _PostPageState extends State { post.id, body, post.lang!, - post.isAdult, + post.isNSFW, ), }; _onUpdate(newPost); diff --git a/lib/src/screens/profile/message_item.dart b/lib/src/screens/profile/message_item.dart index d4cd238..378ad92 100644 --- a/lib/src/screens/profile/message_item.dart +++ b/lib/src/screens/profile/message_item.dart @@ -54,7 +54,7 @@ class MessageItem extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 8), child: Markdown(item.messages.first.body, - context.read().instanceHost), + context.watch().instanceHost), ), ], ), diff --git a/lib/src/screens/profile/profile_edit_screen.dart b/lib/src/screens/profile/profile_edit_screen.dart index 37dd86a..fe23048 100644 --- a/lib/src/screens/profile/profile_edit_screen.dart +++ b/lib/src/screens/profile/profile_edit_screen.dart @@ -11,7 +11,6 @@ import '../../widgets/image_selector.dart'; import '../settings/settings_controller.dart'; class ProfileEditScreen extends StatefulWidget { - final DetailedUserModel user; final void Function(DetailedUserModel?) onUpdate; @@ -22,7 +21,6 @@ class ProfileEditScreen extends StatefulWidget { } class _ProfileEditScreen extends State { - TextEditingController? _aboutTextController; XFile? _avatarFile; bool _deleteAvatar = false; @@ -40,7 +38,8 @@ class _ProfileEditScreen extends State { } void _initSettings() async { - final settings = await context.read().api.users.getUserSettings(); + final settings = + await context.read().api.users.getUserSettings(); setState(() { _settings = settings; }); @@ -52,332 +51,354 @@ class _ProfileEditScreen extends State { appBar: AppBar( actions: [ IconButton( - onPressed: () async { - if (_settingsChanged) { - _settings = await context.read().api.users.saveUserSettings(_settings!); - } - if (!context.mounted) return; + onPressed: () async { + if (_settingsChanged) { + _settings = await context + .read() + .api + .users + .saveUserSettings(_settings!); + } + if (!context.mounted) return; - var user = await context.read().api.users - .updateProfile(_aboutTextController!.text); + var user = await context + .read() + .api + .users + .updateProfile(_aboutTextController!.text); - if (!context.mounted) return; - if (_deleteAvatar) { - user = await context.read().api.users - .deleteAvatar(); - } - if (!context.mounted) return; - if (_deleteCover) { - user = await context.read().api.users - .deleteCover(); - } + if (!context.mounted) return; + if (_deleteAvatar) { + user = await context + .read() + .api + .users + .deleteAvatar(); + } + if (!context.mounted) return; + if (_deleteCover) { + user = await context + .read() + .api + .users + .deleteCover(); + } - if (!context.mounted) return; - if (_avatarFile != null) { - user = await context.read().api.users - .updateAvatar(_avatarFile!); - } - if (!context.mounted) return; - if (_coverFile != null) { - user = await context.read().api.users - .updateCover(_coverFile!); - } - if (!context.mounted) return; + if (!context.mounted) return; + if (_avatarFile != null) { + user = await context + .read() + .api + .users + .updateAvatar(_avatarFile!); + } + if (!context.mounted) return; + if (_coverFile != null) { + user = await context + .read() + .api + .users + .updateCover(_coverFile!); + } + if (!context.mounted) return; - widget.onUpdate(user); - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.send) + widget.onUpdate(user); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.send), ) ], ), body: SingleChildScrollView( - child: Column( - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height / 3, - ), - height: widget.user.cover == null ? 100 : null, - child: _coverFile != null - ? Image.file(File(_coverFile!.path)) - : widget.user.cover != null + child: Column( + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height / 3, + ), + height: widget.user.cover == null ? 100 : null, + child: _coverFile != null + ? Image.file(File(_coverFile!.path)) + : widget.user.cover != null ? _deleteCover - ? null - : Image.network( - widget.user.cover!, - width: double.infinity, - fit: BoxFit.cover, - ) + ? null + : Image.network( + widget.user.cover!, + width: double.infinity, + fit: BoxFit.cover, + ) : null, - ), - Positioned( - left: 0, - bottom: 0, - child: Padding( - padding: const EdgeInsets.all(12), - child: Avatar( - _deleteAvatar ? null : widget.user.avatar, - radius: 32, - borderRadius: 4, - ), + ), + Positioned( + left: 0, + bottom: 0, + child: Padding( + padding: const EdgeInsets.all(12), + child: Avatar( + _deleteAvatar ? null : widget.user.avatar, + radius: 32, + borderRadius: 4, ), ), - ] - ), - Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.user.displayName ?? - widget.user.name.split('@').first, - style: - Theme.of(context).textTheme.titleLarge, - ), - Text( - widget.user.name.contains('@') - ? '@${widget.user.name}' - : '@${widget.user.name}@${context.read().instanceHost}', - ), - ], - ), - ), - ] - ), - Row( - children: [ - const Text("Select Avatar"), - Padding( - padding: const EdgeInsets.all(12), - child: ImageSelector( - _avatarFile, - (file) => setState(() { - _avatarFile = file; - })), - ), - TextButton( - onPressed: () { - setState(() { - _deleteAvatar = true; - }); - }, - child: const Text("Delete") - ) - ], - ), - Row( - children: [ - const Text("Select Cover"), - Padding( - padding: const EdgeInsets.all(12), - child: ImageSelector( - _coverFile, - (file) => setState(() { - _coverFile = file; - }), - ), - ), - TextButton( - onPressed: () { - setState(() { - _deleteCover = true; - }); - }, - child: const Text("Delete"), - ) - ], - ), - Padding( - padding: const EdgeInsets.only(top: 12), - child: TextEditor( - _aboutTextController!, - label: "About", - isMarkdown: true, - ), - ), - if (_settings != null) - Padding( - padding: const EdgeInsets.only(top: 30), + ), + ], + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Expanded( child: Column( - mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Settings", - style: Theme.of(context).textTheme.titleLarge + widget.user.displayName ?? + widget.user.name.split('@').first, + style: Theme.of(context).textTheme.titleLarge, ), - SwitchListTile( - title: const Text("Show NSFW"), - value: _settings!.showNSFW, - onChanged: (bool value) { - setState(() { - _settings!.showNSFW = value; - _settingsChanged = true; - }); - }, + Text( + widget.user.name.contains('@') + ? '@${widget.user.name}' + : '@${widget.user.name}@${context.watch().instanceHost}', ), - if (_settings!.blurNSFW != null) - SwitchListTile( - title: const Text("Blur NSFW"), - value: _settings!.blurNSFW!, - onChanged: (bool? value) { - setState(() { - _settings!.blurNSFW = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showReadPosts != null) - SwitchListTile( - title: const Text("Show read posts"), - value: _settings!.showReadPosts!, - onChanged: (bool? value) { - setState(() { - _settings!.showReadPosts = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showSubscribedUsers != null) - SwitchListTile( - title: const Text("Show subscribed users"), - value: _settings!.showSubscribedUsers!, - onChanged: (bool? value) { - setState(() { - _settings!.showSubscribedUsers = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showSubscribedMagazines != null) - SwitchListTile( - title: const Text("Show subscribed magazines"), - value: _settings!.showSubscribedMagazines!, - onChanged: (bool? value) { - setState(() { - _settings!.showSubscribedMagazines = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showSubscribedDomains != null) - SwitchListTile( - title: const Text("Show subscribed domains"), - value: _settings!.showSubscribedDomains!, - onChanged: (bool? value) { - setState(() { - _settings!.showSubscribedDomains = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showProfileSubscriptions != null) - SwitchListTile( - title: const Text("Show profile subscriptions"), - value: _settings!.showProfileSubscriptions!, - onChanged: (bool? value) { - setState(() { - _settings!.showProfileSubscriptions = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.showProfileFollowings != null) - SwitchListTile( - title: const Text("Show profile followings"), - value: _settings!.showProfileFollowings!, - onChanged: (bool? value) { - setState(() { - _settings!.showProfileFollowings = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewEntry != null) - SwitchListTile( - title: const Text("Notify on new threads in subscribed magazines"), - value: _settings!.notifyOnNewEntry!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewEntry = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewPost != null) - SwitchListTile( - title: const Text("Notify on new micropost in subscribed magazines"), - value: _settings!.notifyOnNewPost!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewPost = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewEntryReply != null) - SwitchListTile( - title: const Text("Notify on comments in authored threads"), - value: _settings!.notifyOnNewEntryReply!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewEntryReply = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewEntryCommentReply != null) - SwitchListTile( - title: const Text("Notify on thread comment reply"), - value: _settings!.notifyOnNewEntryCommentReply!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewEntryCommentReply = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewPostReply != null) - SwitchListTile( - title: const Text("Notify on comments in authored microposts"), - value: _settings!.notifyOnNewPostReply!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewPostReply = value!; - _settingsChanged = true; - }); - } - ), - if (_settings!.notifyOnNewPostCommentReply != null) - SwitchListTile( - title: const Text("Notify on micropost comment reply"), - value: _settings!.notifyOnNewPostCommentReply!, - onChanged: (bool? value) { - setState(() { - _settings!.notifyOnNewPostCommentReply = value!; - _settingsChanged = true; - }); - } - ), ], + ), + ), + ]), + Row( + children: [ + const Text("Select Avatar"), + Padding( + padding: const EdgeInsets.all(12), + child: ImageSelector( + _avatarFile, + (file) => setState(() { + _avatarFile = file; + })), + ), + TextButton( + onPressed: () { + setState(() { + _deleteAvatar = true; + }); + }, + child: const Text("Delete"), ) - ) - ], - ), + ], + ), + Row( + children: [ + const Text("Select Cover"), + Padding( + padding: const EdgeInsets.all(12), + child: ImageSelector( + _coverFile, + (file) => setState(() { + _coverFile = file; + }), + ), + ), + TextButton( + onPressed: () { + setState(() { + _deleteCover = true; + }); + }, + child: const Text("Delete"), + ) + ], + ), + Padding( + padding: const EdgeInsets.only(top: 12), + child: TextEditor( + _aboutTextController!, + label: "About", + isMarkdown: true, + ), + ), + if (_settings != null) + Padding( + padding: const EdgeInsets.only(top: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "Settings", + style: Theme.of(context).textTheme.titleLarge, + ), + SwitchListTile( + title: const Text("Show NSFW"), + value: _settings!.showNSFW, + onChanged: (bool value) { + setState(() { + _settings!.showNSFW = value; + _settingsChanged = true; + }); + }, + ), + if (_settings!.blurNSFW != null) + SwitchListTile( + title: const Text("Blur NSFW"), + value: _settings!.blurNSFW!, + onChanged: (bool? value) { + setState(() { + _settings!.blurNSFW = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showReadPosts != null) + SwitchListTile( + title: const Text("Show read posts"), + value: _settings!.showReadPosts!, + onChanged: (bool? value) { + setState(() { + _settings!.showReadPosts = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showSubscribedUsers != null) + SwitchListTile( + title: const Text("Show subscribed users"), + value: _settings!.showSubscribedUsers!, + onChanged: (bool? value) { + setState(() { + _settings!.showSubscribedUsers = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showSubscribedMagazines != null) + SwitchListTile( + title: const Text("Show subscribed magazines"), + value: _settings!.showSubscribedMagazines!, + onChanged: (bool? value) { + setState(() { + _settings!.showSubscribedMagazines = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showSubscribedDomains != null) + SwitchListTile( + title: const Text("Show subscribed domains"), + value: _settings!.showSubscribedDomains!, + onChanged: (bool? value) { + setState(() { + _settings!.showSubscribedDomains = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showProfileSubscriptions != null) + SwitchListTile( + title: const Text("Show profile subscriptions"), + value: _settings!.showProfileSubscriptions!, + onChanged: (bool? value) { + setState(() { + _settings!.showProfileSubscriptions = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.showProfileFollowings != null) + SwitchListTile( + title: const Text("Show profile followings"), + value: _settings!.showProfileFollowings!, + onChanged: (bool? value) { + setState(() { + _settings!.showProfileFollowings = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewEntry != null) + SwitchListTile( + title: const Text( + "Notify on new threads in subscribed magazines"), + value: _settings!.notifyOnNewEntry!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewEntry = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewPost != null) + SwitchListTile( + title: const Text( + "Notify on new microblog in subscribed magazines"), + value: _settings!.notifyOnNewPost!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewPost = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewEntryReply != null) + SwitchListTile( + title: const Text( + "Notify on comments in authored threads"), + value: _settings!.notifyOnNewEntryReply!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewEntryReply = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewEntryCommentReply != null) + SwitchListTile( + title: + const Text("Notify on thread comment reply"), + value: _settings!.notifyOnNewEntryCommentReply!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewEntryCommentReply = + value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewPostReply != null) + SwitchListTile( + title: const Text( + "Notify on comments in authored microposts"), + value: _settings!.notifyOnNewPostReply!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewPostReply = value!; + _settingsChanged = true; + }); + }, + ), + if (_settings!.notifyOnNewPostCommentReply != null) + SwitchListTile( + title: const Text( + "Notify on microblog comment reply"), + value: _settings!.notifyOnNewPostCommentReply!, + onChanged: (bool? value) { + setState(() { + _settings!.notifyOnNewPostCommentReply = + value!; + _settingsChanged = true; + }); + }, + ), + ], + )) + ], ), - ], - ) - ), + ), + ], + )), ); } -} \ No newline at end of file +} diff --git a/lib/src/screens/settings/general_settings.dart b/lib/src/screens/settings/general_settings.dart index 1e04af0..40d75db 100644 --- a/lib/src/screens/settings/general_settings.dart +++ b/lib/src/screens/settings/general_settings.dart @@ -17,7 +17,8 @@ class GeneralScreen extends StatelessWidget { final currentThemeMode = themeModeSelect.getOption(controller.themeMode); final currentTheme = themeSelect.getOption(controller.accentColor); - final currentPostLayout = postLayoutSelect.getOption(controller.postLayout); + final currentPostImagePosition = + postLayoutSelect.getOption(controller.postImagePosition); final customLanguageFilterEnabled = !controller.useAccountLangFilter && !isLemmy; @@ -86,6 +87,67 @@ class GeneralScreen extends StatelessWidget { ), enabled: !controller.useDynamicColor, ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text('Post Appearance', + style: Theme.of(context).textTheme.titleMedium), + ), + ListTile( + title: const Text('Image Position'), + leading: const Icon(Icons.image), + onTap: () async { + controller.updatePostImagePosition( + await postLayoutSelect.askSelection( + context, + controller.postImagePosition, + ), + ); + }, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(currentPostImagePosition.icon), + const SizedBox(width: 4), + Text(currentPostImagePosition.title), + ], + ), + ), + ListTile( + title: const Text('Limit Title Preview'), + leading: const Icon(Icons.title), + onTap: () { + controller.updatePostLimitTitlePreview( + !controller.postLimitTitlePreview); + }, + trailing: Switch( + value: controller.postLimitTitlePreview, + onChanged: controller.updatePostLimitTitlePreview, + ), + ), + ListTile( + title: const Text('Show Text Preview'), + leading: const Icon(Icons.description), + onTap: () { + controller + .updatePostShowTextPreview(!controller.postShowTextPreview); + }, + trailing: Switch( + value: controller.postShowTextPreview, + onChanged: controller.updatePostShowTextPreview, + ), + ), + ListTile( + title: const Text('Use Card Preview'), + leading: const Icon(Icons.view_agenda), + onTap: () { + controller + .updatePostUseCardPreview(!controller.postUseCardPreview); + }, + trailing: Switch( + value: controller.postUseCardPreview, + onChanged: controller.updatePostUseCardPreview, + ), + ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text('Language', @@ -179,26 +241,6 @@ class GeneralScreen extends StatelessWidget { subtitle: const Text( 'When enabled, the instance of a user/magazine will always display instead of an @ button'), ), - ListTile( - title: const Text('Post Layout'), - leading: const Icon(Icons.view_list), - onTap: () async { - controller.updatePostLayout( - await postLayoutSelect.askSelection( - context, - controller.postLayout, - ), - ); - }, - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(currentPostLayout.icon), - const SizedBox(width: 4), - Text(currentPostLayout.title), - ], - ), - ), ], ), ); @@ -238,22 +280,22 @@ SelectionMenu themeSelect = SelectionMenu( .toList(), ); -const SelectionMenu postLayoutSelect = SelectionMenu( - 'Post Layout', +const SelectionMenu postLayoutSelect = SelectionMenu( + 'Post Image Position', [ SelectionMenuItem( - value: PostLayout.auto, + value: PostImagePosition.auto, title: 'Auto', icon: Icons.auto_mode, ), SelectionMenuItem( - value: PostLayout.narrow, - title: 'Narrow', + value: PostImagePosition.top, + title: 'Top', icon: Icons.smartphone, ), SelectionMenuItem( - value: PostLayout.wide, - title: 'Wide', + value: PostImagePosition.right, + title: 'Right', icon: Icons.tablet, ), ], diff --git a/lib/src/screens/settings/settings_controller.dart b/lib/src/screens/settings/settings_controller.dart index e80fc8a..368d9f2 100644 --- a/lib/src/screens/settings/settings_controller.dart +++ b/lib/src/screens/settings/settings_controller.dart @@ -17,7 +17,7 @@ import 'package:shared_preferences/shared_preferences.dart'; enum ServerSoftware { kbin, mbin, lemmy } -enum PostLayout { auto, narrow, wide } +enum PostImagePosition { auto, top, right } class Server { final ServerSoftware software; @@ -65,10 +65,17 @@ class SettingsController with ChangeNotifier { ThemeInfo get theme => themes.firstWhere((theme) => theme.name == _accentColor); + late PostImagePosition _postImagePosition; + PostImagePosition get postImagePosition => _postImagePosition; + late bool _postLimitTitlePreview; + bool get postLimitTitlePreview => _postLimitTitlePreview; + late bool _postShowTextPreview; + bool get postShowTextPreview => _postShowTextPreview; + late bool _postUseCardPreview; + bool get postUseCardPreview => _postUseCardPreview; + late bool _alwaysShowInstance; bool get alwaysShowInstance => _alwaysShowInstance; - late PostLayout _postLayout; - PostLayout get postLayout => _postLayout; late ActionLocation _feedActionBackToTop; ActionLocation get feedActionBackToTop => _feedActionBackToTop; @@ -124,21 +131,19 @@ class SettingsController with ChangeNotifier { ThemeMode.system, prefs.getString("themeMode"), ); - _useDynamicColor = prefs.getBool("useDynamicColor") != null - ? prefs.getBool("useDynamicColor")! - : true; - _accentColor = prefs.getString("accentColor") != null - ? prefs.getString("accentColor")! - : "Default"; + _useDynamicColor = prefs.getBool("useDynamicColor") ?? true; + _accentColor = prefs.getString("accentColor") ?? "Default"; - _alwaysShowInstance = prefs.getBool("alwaysShowInstance") != null - ? prefs.getBool("alwaysShowInstance")! - : false; - _postLayout = parseEnum( - PostLayout.values, - PostLayout.auto, - prefs.getString("postLayout"), + _alwaysShowInstance = prefs.getBool("alwaysShowInstance") ?? false; + + _postImagePosition = parseEnum( + PostImagePosition.values, + PostImagePosition.auto, + prefs.getString("postImagePosition"), ); + _postLimitTitlePreview = prefs.getBool("postLimitTitlePreview") ?? false; + _postShowTextPreview = prefs.getBool("postShowTextPreview") ?? true; + _postUseCardPreview = prefs.getBool("postUseCardPreview") ?? true; _feedActionBackToTop = parseEnum( ActionLocation.values, @@ -202,15 +207,9 @@ class SettingsController with ChangeNotifier { prefs.getString("defaultCommentSort"), ); - _useAccountLangFilter = prefs.getBool("useAccountLangFilter") != null - ? prefs.getBool("useAccountLangFilter")! - : true; - _langFilter = prefs.getStringList("langFilter") != null - ? prefs.getStringList("langFilter")!.toSet() - : {}; - _defaultCreateLang = prefs.getString("defaultCreateLang") != null - ? prefs.getString("defaultCreateLang")! - : 'en'; + _useAccountLangFilter = prefs.getBool("useAccountLangFilter") ?? true; + _langFilter = prefs.getStringList("langFilter")?.toSet() ?? {}; + _defaultCreateLang = prefs.getString("defaultCreateLang") ?? 'en'; _servers = (jsonDecode(prefs.getString('servers') ?? '{"kbin.earth":{"software":"mbin"}}') as Map) @@ -224,144 +223,208 @@ class SettingsController with ChangeNotifier { notifyListeners(); } - Future updateThemeMode(ThemeMode? newThemeMode) async { - if (newThemeMode == null) return; - if (newThemeMode == _themeMode) return; - - _themeMode = newThemeMode; + Future presetClassic() async { + _postImagePosition = PostImagePosition.auto; + _postLimitTitlePreview = false; + _postShowTextPreview = true; + _postUseCardPreview = true; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('themeMode', newThemeMode.name); + await prefs.setString('postImagePosition', _postImagePosition.name); + await prefs.setBool('postLimitTitlePreview', _postLimitTitlePreview); + await prefs.setBool('postShowTextPreview', _postShowTextPreview); + await prefs.setBool('postUseCardPreview', _postUseCardPreview); } - Future updateUseDynamicColor(bool? newUseDynamicColor) async { - if (newUseDynamicColor == null) return; - if (newUseDynamicColor == _useDynamicColor) return; - - _useDynamicColor = newUseDynamicColor; + Future presetCompact() async { + _postImagePosition = PostImagePosition.right; + _postLimitTitlePreview = true; + _postShowTextPreview = false; + _postUseCardPreview = false; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setBool('useDynamicColor', newUseDynamicColor); + await prefs.setString('postImagePosition', _postImagePosition.name); + await prefs.setBool('postLimitTitlePreview', _postLimitTitlePreview); + await prefs.setBool('postShowTextPreview', _postShowTextPreview); + await prefs.setBool('postUseCardPreview', _postUseCardPreview); } - Future updateAccentColor(String? newThemeAccent) async { - if (newThemeAccent == null) return; - if (newThemeAccent == _accentColor) return; + Future updateThemeMode(ThemeMode? newValue) async { + if (newValue == null) return; + if (newValue == _themeMode) return; - _accentColor = newThemeAccent; + _themeMode = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('accentColor', newThemeAccent); + await prefs.setString('themeMode', newValue.name); } - Future updateAlwaysShowInstance(bool? newShowDisplayInstance) async { - if (newShowDisplayInstance == null) return; - if (newShowDisplayInstance == _alwaysShowInstance) return; + Future updateUseDynamicColor(bool? newValue) async { + if (newValue == null) return; + if (newValue == _useDynamicColor) return; - _alwaysShowInstance = newShowDisplayInstance; + _useDynamicColor = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setBool('alwaysShowInstance', newShowDisplayInstance); + await prefs.setBool('useDynamicColor', newValue); } - Future updatePostLayout(PostLayout? newPostLayout) async { - if (newPostLayout == null) return; - if (newPostLayout == _postLayout) return; + Future updateAccentColor(String? newValue) async { + if (newValue == null) return; + if (newValue == _accentColor) return; - _postLayout = newPostLayout; + _accentColor = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('postLayout', newPostLayout.name); + await prefs.setString('accentColor', newValue); } - Future updateDefaultFeedType(PostType? newDefaultFeedMode) async { - if (newDefaultFeedMode == null) return; - if (newDefaultFeedMode == _defaultFeedType) return; + Future updatePostImagePosition(PostImagePosition? newValue) async { + if (newValue == null) return; + if (newValue == _postImagePosition) return; - _defaultFeedType = newDefaultFeedMode; + _postImagePosition = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('defaultFeedType', newDefaultFeedMode.name); + await prefs.setString('postImagePosition', newValue.name); } - Future updateDefaultEntriesFeedSort( - FeedSort? newDefaultFeedSort) async { - if (newDefaultFeedSort == null) return; - if (newDefaultFeedSort == _defaultEntriesFeedSort) return; + Future updatePostLimitTitlePreview(bool? newValue) async { + if (newValue == null) return; + if (newValue == _postLimitTitlePreview) return; - _defaultEntriesFeedSort = newDefaultFeedSort; + _postLimitTitlePreview = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('defaultFeedSortEntries', newDefaultFeedSort.name); + await prefs.setBool('postLimitTitlePreview', newValue); } - Future updateDefaultPostsFeedSort(FeedSort? newDefaultFeedSort) async { - if (newDefaultFeedSort == null) return; - if (newDefaultFeedSort == _defaultPostsFeedSort) return; + Future updatePostShowTextPreview(bool? newValue) async { + if (newValue == null) return; + if (newValue == _postShowTextPreview) return; - _defaultPostsFeedSort = newDefaultFeedSort; + _postShowTextPreview = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('defaultPostsFeedSort', newDefaultFeedSort.name); + await prefs.setBool('postShowTextPreview', newValue); + } + + Future updatePostUseCardPreview(bool? newValue) async { + if (newValue == null) return; + if (newValue == _postUseCardPreview) return; + + _postUseCardPreview = newValue; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool('postUseCardPreview', newValue); + } + + Future updateAlwaysShowInstance(bool? newValue) async { + if (newValue == null) return; + if (newValue == _alwaysShowInstance) return; + + _alwaysShowInstance = newValue; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool('alwaysShowInstance', newValue); + } + + Future updateDefaultFeedType(PostType? newValue) async { + if (newValue == null) return; + if (newValue == _defaultFeedType) return; + + _defaultFeedType = newValue; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('defaultFeedType', newValue.name); + } + + Future updateDefaultEntriesFeedSort(FeedSort? newValue) async { + if (newValue == null) return; + if (newValue == _defaultEntriesFeedSort) return; + + _defaultEntriesFeedSort = newValue; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('defaultFeedSortEntries', newValue.name); + } + + Future updateDefaultPostsFeedSort(FeedSort? newValue) async { + if (newValue == null) return; + if (newValue == _defaultPostsFeedSort) return; + + _defaultPostsFeedSort = newValue; + + notifyListeners(); + + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('defaultPostsFeedSort', newValue.name); } Future updateDefaultExploreFeedSort( - FeedSort? newDefaultExploreFeedSort, + FeedSort? newValue, ) async { - if (newDefaultExploreFeedSort == null) return; - if (newDefaultExploreFeedSort == _defaultExploreFeedSort) return; + if (newValue == null) return; + if (newValue == _defaultExploreFeedSort) return; - _defaultExploreFeedSort = newDefaultExploreFeedSort; + _defaultExploreFeedSort = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString( - 'defaultExploreFeedSort', newDefaultExploreFeedSort.name); + await prefs.setString('defaultExploreFeedSort', newValue.name); } Future updateDefaultCommentSort( - CommentSort? newDefaultCommentSort, + CommentSort? newValue, ) async { - if (newDefaultCommentSort == null) return; - if (newDefaultCommentSort == _defaultCommentSort) return; + if (newValue == null) return; + if (newValue == _defaultCommentSort) return; - _defaultCommentSort = newDefaultCommentSort; + _defaultCommentSort = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('defaultCommentSort', newDefaultCommentSort.name); + await prefs.setString('defaultCommentSort', newValue.name); } Future updateUseAccountLangFilter( - bool? newUseAccountLangFilter, + bool? newValue, ) async { - if (newUseAccountLangFilter == null) return; - if (newUseAccountLangFilter == _useAccountLangFilter) return; + if (newValue == null) return; + if (newValue == _useAccountLangFilter) return; - _useAccountLangFilter = newUseAccountLangFilter; + _useAccountLangFilter = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setBool('useAccountLangFilter', newUseAccountLangFilter); + await prefs.setBool('useAccountLangFilter', newValue); } Future addLangFilter( @@ -393,17 +456,17 @@ class SettingsController with ChangeNotifier { } Future updateDefaultCreateLang( - String? newDefaultCreateLang, + String? newValue, ) async { - if (newDefaultCreateLang == null) return; - if (newDefaultCreateLang == _defaultCreateLang) return; + if (newValue == null) return; + if (newValue == _defaultCreateLang) return; - _defaultCreateLang = newDefaultCreateLang; + _defaultCreateLang = newValue; notifyListeners(); final SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('defaultCreateLang', newDefaultCreateLang); + await prefs.setString('defaultCreateLang', newValue); } Future saveServer(ServerSoftware software, String server) async { diff --git a/lib/src/screens/settings/settings_screen.dart b/lib/src/screens/settings/settings_screen.dart index eedc014..51b334c 100644 --- a/lib/src/screens/settings/settings_screen.dart +++ b/lib/src/screens/settings/settings_screen.dart @@ -44,6 +44,31 @@ class SettingsScreen extends StatelessWidget { ); }, ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: + Text('Presets', style: Theme.of(context).textTheme.titleMedium), + ), + ListTile( + title: const Text('Classic Layout'), + onTap: () async { + controller.presetClassic(); + + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Preset applied'), + )); + }, + ), + ListTile( + title: const Text('Compact Layout'), + onTap: () async { + controller.presetCompact(); + + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Preset applied'), + )); + }, + ), Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text('Accounts', diff --git a/lib/src/widgets/blur.dart b/lib/src/widgets/blur.dart new file mode 100644 index 0000000..6539d57 --- /dev/null +++ b/lib/src/widgets/blur.dart @@ -0,0 +1,48 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class Blur extends StatelessWidget { + const Blur( + this.child, { + super.key, + this.blur = 16, + this.blurColor = Colors.white, + this.borderRadius, + this.colorOpacity = 0.2, + this.overlay, + this.alignment = Alignment.center, + }); + + final Widget child; + final double blur; + final Color blurColor; + final BorderRadius? borderRadius; + final double colorOpacity; + final Widget? overlay; + final AlignmentGeometry alignment; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: borderRadius ?? BorderRadius.zero, + child: Stack( + children: [ + child, + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur), + child: Container( + decoration: BoxDecoration( + color: blurColor.withOpacity(colorOpacity), + ), + alignment: alignment, + child: overlay, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/widgets/content_item.dart b/lib/src/widgets/content_item.dart index 2a88dcd..a8a0655 100644 --- a/lib/src/widgets/content_item.dart +++ b/lib/src/widgets/content_item.dart @@ -4,6 +4,7 @@ import 'package:interstellar/src/screens/explore/magazine_screen.dart'; import 'package:interstellar/src/screens/explore/user_screen.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; +import 'package:interstellar/src/widgets/blur.dart'; import 'package:interstellar/src/widgets/display_name.dart'; import 'package:interstellar/src/widgets/markdown.dart'; import 'package:interstellar/src/widgets/open_webpage.dart'; @@ -22,10 +23,14 @@ class ContentItem extends StatefulWidget { final Uri? video; final String? body; final DateTime? createdAt; + final DateTime? editedAt; final bool isPreview; final bool showMagazineFirst; + final bool isNSFW; + final bool isOC; + final String? user; final String? userIcon; final int? userIdOnClick; @@ -69,8 +74,11 @@ class ContentItem extends StatefulWidget { this.video, this.body, this.createdAt, + this.editedAt, this.isPreview = false, this.showMagazineFirst = false, + this.isNSFW = false, + this.isOC = false, this.user, this.userIcon, this.userIdOnClick, @@ -139,18 +147,38 @@ class _ContentItemState extends State { final Widget? userWidget = widget.user != null ? Padding( padding: const EdgeInsets.only(right: 10), - child: DisplayName( - widget.user!, - icon: widget.userIcon, - onTap: widget.userIdOnClick != null - ? () => Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => UserScreen( - widget.userIdOnClick!, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + DisplayName( + widget.user!, + icon: widget.userIcon, + onTap: widget.userIdOnClick != null + ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => UserScreen( + widget.userIdOnClick!, + ), + ), + ) + : null, + ), + if (widget.opUserId == widget.userIdOnClick) + const Padding( + padding: EdgeInsets.only(left: 5), + child: Tooltip( + message: 'Original Poster', + triggerMode: TooltipTriggerMode.tap, + child: Text( + 'OP', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.bold, ), - ) - : null, + ), + ), + ), + ], ), ) : null; @@ -174,15 +202,56 @@ class _ContentItemState extends State { : null; return LayoutBuilder(builder: (context, constrains) { - final isWide = switch (context.watch().postLayout) { - PostLayout.auto => constrains.maxWidth > 800, - PostLayout.narrow => false, - PostLayout.wide => true, + final hasWideSize = constrains.maxWidth > 800; + final isRightImage = + switch (context.watch().postImagePosition) { + PostImagePosition.auto => hasWideSize, + PostImagePosition.top => false, + PostImagePosition.right => true, }; + final double rightImageSize = hasWideSize ? 128 : 64; + + final imageWidget = widget.image == null + ? null + : Wrapper( + shouldWrap: widget.video == null, + parentBuilder: (child) => InkWell( + onTap: () => _onImageClick(context), + child: child, + ), + child: Wrapper( + shouldWrap: widget.isNSFW, + parentBuilder: (child) => Blur(child), + child: isRightImage + ? Image.network( + widget.image!, + height: rightImageSize, + width: rightImageSize, + fit: BoxFit.cover, + ) + : (widget.isPreview + ? Image.network( + widget.image!, + height: 160, + width: double.infinity, + fit: BoxFit.cover, + ) + : Image.network(widget.image!)), + ), + ); + + final titleStyle = hasWideSize + ? Theme.of(context).textTheme.titleLarge! + : Theme.of(context).textTheme.titleMedium!; + final titleOverflow = widget.isPreview && + context.watch().postLimitTitlePreview + ? TextOverflow.ellipsis + : null; + return Column( children: [ - if ((!isWide && widget.image != null) || + if ((!isRightImage && imageWidget != null) || (!widget.isPreview && widget.video != null)) Wrapper( shouldWrap: !widget.isPreview, @@ -193,21 +262,7 @@ class _ContentItemState extends State { child: child), child: (!widget.isPreview && widget.video != null) ? VideoPlayer(widget.video!) - : Wrapper( - shouldWrap: widget.video == null, - parentBuilder: (child) => InkWell( - onTap: () => _onImageClick(context), - child: child, - ), - child: widget.isPreview - ? Image.network( - widget.image!, - height: 160, - width: double.infinity, - fit: BoxFit.cover, - ) - : Image.network(widget.image!), - ), + : imageWidget!, ), Container( padding: const EdgeInsets.all(12), @@ -216,7 +271,7 @@ class _ContentItemState extends State { children: [ Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (widget.title != null) Padding( @@ -225,12 +280,9 @@ class _ContentItemState extends State { ? InkWell( child: Text( widget.title!, - style: Theme.of(context) - .textTheme - .titleLarge! - .apply( - decoration: - TextDecoration.underline), + style: titleStyle.apply( + decoration: TextDecoration.underline), + overflow: titleOverflow, ), onTap: () { openWebpage(context, widget.link!); @@ -238,37 +290,63 @@ class _ContentItemState extends State { ) : Text( widget.title!, - style: Theme.of(context).textTheme.titleLarge, + style: titleStyle, + overflow: titleOverflow, ), ), Row( children: [ + if (widget.isNSFW) + const Padding( + padding: EdgeInsets.only(right: 10), + child: Tooltip( + message: 'Not Safe For Work', + triggerMode: TooltipTriggerMode.tap, + child: Text( + 'NSFW', + style: TextStyle( + color: Colors.red, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + if (widget.isOC) + const Padding( + padding: EdgeInsets.only(right: 10), + child: Tooltip( + message: 'Original Content', + triggerMode: TooltipTriggerMode.tap, + child: Text( + 'OC', + style: TextStyle( + color: Colors.lightGreen, + fontWeight: FontWeight.bold, + ), + ), + ), + ), if (!widget.showMagazineFirst && userWidget != null) userWidget, - if (!widget.showMagazineFirst && - widget.opUserId == widget.userIdOnClick) - const Padding( - padding: EdgeInsets.only(right: 10), - child: Text("OP")), if (widget.showMagazineFirst && magazineWidget != null) magazineWidget, if (widget.createdAt != null) Padding( padding: const EdgeInsets.only(right: 10), - child: Text( - timeDiffFormat(widget.createdAt!), - style: const TextStyle( - fontWeight: FontWeight.w300), + child: Tooltip( + message: + 'Created: ${widget.createdAt!.toIso8601String()}${widget.editedAt == null ? '' : '\nEdited: ${widget.editedAt!.toIso8601String()}'}', + triggerMode: TooltipTriggerMode.tap, + child: Text( + timeDiffFormat(widget.createdAt!), + style: const TextStyle( + fontWeight: FontWeight.w300), + ), ), ), if (widget.showMagazineFirst && userWidget != null) userWidget, - if (widget.showMagazineFirst && - widget.opUserId == widget.userIdOnClick) - const Padding( - padding: EdgeInsets.only(right: 10), - child: Text("OP")), if (!widget.showMagazineFirst && magazineWidget != null) magazineWidget, @@ -295,210 +373,222 @@ class _ContentItemState extends State { ), ], ), - if (widget.body != null) const SizedBox(height: 10), - if (widget.body != null) - widget.isPreview - ? Text( - widget.body!, - maxLines: 4, - overflow: TextOverflow.ellipsis, - ) - : Markdown(widget.body!, widget.originInstance), - const SizedBox(height: 10), - LayoutBuilder(builder: (context, constrains) { - final votingWidgets = [ - if (widget.boosts != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: Row( + if (widget.body != null && + widget.body!.isNotEmpty && + (!widget.isPreview || + context + .watch() + .postShowTextPreview)) + Padding( + padding: const EdgeInsets.only(top: 10), + child: widget.isPreview + ? Text( + widget.body!, + maxLines: 4, + overflow: TextOverflow.ellipsis, + ) + : Markdown( + widget.body!, widget.originInstance)), + Padding( + padding: const EdgeInsets.only(top: 10), + child: LayoutBuilder(builder: (context, constrains) { + final votingWidgets = [ + if (widget.boosts != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.rocket_launch), + color: widget.isBoosted + ? Colors.purple.shade400 + : null, + onPressed: widget.onBoost, + ), + Text(intFormat(widget.boosts!)) + ], + ), + ), + if (widget.upVotes != null || + widget.downVotes != null) + Row( children: [ - IconButton( - icon: const Icon(Icons.rocket_launch), - color: widget.isBoosted - ? Colors.purple.shade400 - : null, - onPressed: widget.onBoost, - ), - Text(intFormat(widget.boosts!)) + if (widget.upVotes != null) + IconButton( + icon: const Icon(Icons.arrow_upward), + color: widget.isUpVoted + ? Colors.green.shade400 + : null, + onPressed: widget.onUpVote, + ), + Text(intFormat((widget.upVotes ?? 0) - + (widget.downVotes ?? 0))), + if (widget.downVotes != null) + IconButton( + icon: const Icon(Icons.arrow_downward), + color: widget.isDownVoted + ? Colors.red.shade400 + : null, + onPressed: widget.onDownVote, + ), ], ), - ), - if (widget.upVotes != null || - widget.downVotes != null) - Row( - children: [ - if (widget.upVotes != null) - IconButton( - icon: const Icon(Icons.arrow_upward), - color: widget.isUpVoted - ? Colors.green.shade400 - : null, - onPressed: widget.onUpVote, - ), - Text(intFormat((widget.upVotes ?? 0) - - (widget.downVotes ?? 0))), - if (widget.downVotes != null) - IconButton( - icon: const Icon(Icons.arrow_downward), - color: widget.isDownVoted - ? Colors.red.shade400 - : null, - onPressed: widget.onDownVote, - ), - ], - ), - ]; - final commentWidgets = [ - if (widget.numComments != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: Row( - children: [ - const Icon(Icons.comment), - const SizedBox(width: 4), - Text(intFormat(widget.numComments!)) - ], + ]; + final commentWidgets = [ + if (widget.numComments != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + const Icon(Icons.comment), + const SizedBox(width: 4), + Text(intFormat(widget.numComments!)) + ], + ), ), - ), - if (widget.onReply != null) - Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: const Icon(Icons.reply), - onPressed: () => setState(() { - _replyTextController = - TextEditingController(); - }), + if (widget.onReply != null) + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: const Icon(Icons.reply), + onPressed: () => setState(() { + _replyTextController = + TextEditingController(); + }), + ), ), - ), - if (widget.onCollapse != null) - IconButton( - tooltip: - widget.isCollapsed ? 'Expand' : 'Collapse', - onPressed: widget.onCollapse, - icon: widget.isCollapsed - ? const Icon(Icons.expand_more) - : const Icon(Icons.expand_less)), - ]; - final menuWidgets = [ - if (widget.openLinkUri != null || - widget.onReport != null || - widget.onEdit != null || - widget.onDelete != null) - MenuAnchor( - builder: (BuildContext context, - MenuController controller, Widget? child) { - return IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () { - if (_menuController.isOpen) { - _menuController.close(); - } else { - _menuController.open(); - } - }, - ); - }, - controller: _menuController, - menuChildren: [ - if (widget.openLinkUri != null) - MenuItemButton( - onPressed: () => openWebpage( - context, widget.openLinkUri!), - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Open Link")), - ), - if (widget.onReport != null) - MenuItemButton( - onPressed: () async { - final reportReason = await reportContent( - context, widget.contentTypeName); - - if (reportReason != null) { - await widget.onReport!(reportReason); + if (widget.onCollapse != null) + IconButton( + tooltip: widget.isCollapsed + ? 'Expand' + : 'Collapse', + onPressed: widget.onCollapse, + icon: widget.isCollapsed + ? const Icon(Icons.expand_more) + : const Icon(Icons.expand_less)), + ]; + final menuWidgets = [ + if (widget.openLinkUri != null || + widget.onReport != null || + widget.onEdit != null || + widget.onDelete != null) + MenuAnchor( + builder: (BuildContext context, + MenuController controller, Widget? child) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + if (_menuController.isOpen) { + _menuController.close(); + } else { + _menuController.open(); } }, - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Report")), - ), - if (widget.onEdit != null) - MenuItemButton( - onPressed: () => setState(() { - _editTextController = - TextEditingController( - text: widget.body); - }), - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Edit")), - ), - if (widget.onDelete != null) - MenuItemButton( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => - AlertDialog( - title: Text( - 'Delete ${widget.contentTypeName}'), - actions: [ - OutlinedButton( - onPressed: () => - Navigator.pop(context), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: () { - Navigator.pop(context); - - widget.onDelete!(); - }, - child: const Text('Delete'), - ), - ], - actionsOverflowAlignment: - OverflowBarAlignment.center, - actionsOverflowButtonSpacing: 8, - actionsOverflowDirection: - VerticalDirection.up, - ), + ); + }, + controller: _menuController, + menuChildren: [ + if (widget.openLinkUri != null) + MenuItemButton( + onPressed: () => openWebpage( + context, widget.openLinkUri!), + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Open Link")), ), - child: const Padding( - padding: EdgeInsets.all(12), - child: Text("Delete")), - ), - ], - ), - ]; + if (widget.onReport != null) + MenuItemButton( + onPressed: () async { + final reportReason = + await reportContent(context, + widget.contentTypeName); - return constrains.maxWidth < 300 - ? Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: votingWidgets, - ), - const SizedBox(height: 4), - Row( - children: [ - ...commentWidgets, - const Spacer(), - ...menuWidgets, - ], - ), + if (reportReason != null) { + await widget.onReport!(reportReason); + } + }, + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Report")), + ), + if (widget.onEdit != null) + MenuItemButton( + onPressed: () => setState(() { + _editTextController = + TextEditingController( + text: widget.body); + }), + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Edit")), + ), + if (widget.onDelete != null) + MenuItemButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => + AlertDialog( + title: Text( + 'Delete ${widget.contentTypeName}'), + actions: [ + OutlinedButton( + onPressed: () => + Navigator.pop(context), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + Navigator.pop(context); + + widget.onDelete!(); + }, + child: const Text('Delete'), + ), + ], + actionsOverflowAlignment: + OverflowBarAlignment.center, + actionsOverflowButtonSpacing: 8, + actionsOverflowDirection: + VerticalDirection.up, + ), + ), + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Delete")), + ), ], - ) - : Row( - children: [ - ...commentWidgets, - const Spacer(), - ...menuWidgets, - const SizedBox(width: 8), - ...votingWidgets, - ], - ); - }), + ), + ]; + + return constrains.maxWidth < 300 + ? Column( + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: votingWidgets, + ), + const SizedBox(height: 4), + Row( + children: [ + ...commentWidgets, + const Spacer(), + ...menuWidgets, + ], + ), + ], + ) + : Row( + children: [ + ...commentWidgets, + const Spacer(), + ...menuWidgets, + const SizedBox(width: 8), + ...votingWidgets, + ], + ); + }), + ), if (widget.onReply != null && _replyTextController != null) Padding( @@ -574,24 +664,12 @@ class _ContentItemState extends State { ], ), ), - if (isWide && widget.image != null) + if (isRightImage && imageWidget != null) Padding( padding: const EdgeInsets.only(left: 16), child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: Wrapper( - shouldWrap: widget.video == null, - parentBuilder: (child) => InkWell( - onTap: () => _onImageClick(context), - child: child, - ), - child: Image.network( - widget.image!, - height: 128, - width: 128, - fit: BoxFit.cover, - ), - ), + child: imageWidget, ), ), ],