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
This commit is contained in:
John Wesley 2024-05-06 16:10:13 -04:00 committed by GitHub
parent 13d3010976
commit 8e0811796c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1032 additions and 735 deletions

View File

@ -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<CommentModel>? 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<dynamic>)
@ -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,

View File

@ -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),

View File

@ -113,7 +113,7 @@ class _MagazinesScreenState extends State<MagazinesScreen> {
],
),
),
...(context.read<SettingsController>().serverSoftware ==
...(context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy ||
filter == APIMagazinesFilter.all ||
filter == APIMagazinesFilter.local

View File

@ -71,8 +71,8 @@ class _FeedScreenState extends State<FeedScreen> {
final actions = [
feedActionCreatePost.withProps(
context.read<SettingsController>().isLoggedIn
? context.read<SettingsController>().feedActionCreatePost
context.watch<SettingsController>().isLoggedIn
? context.watch<SettingsController>().feedActionCreatePost
: ActionLocation.hide,
() async {
await Navigator.of(context).push(
@ -92,7 +92,7 @@ class _FeedScreenState extends State<FeedScreen> {
: parseEnum(
ActionLocation.values,
ActionLocation.hide,
context.read<SettingsController>().feedActionSetFilter.name,
context.watch<SettingsController>().feedActionSetFilter.name,
),
() async {
final newFilter =
@ -109,7 +109,7 @@ class _FeedScreenState extends State<FeedScreen> {
parseEnum(
ActionLocation.values,
ActionLocation.hide,
context.read<SettingsController>().feedActionSetSort.name,
context.watch<SettingsController>().feedActionSetSort.name,
),
() async {
final newSort = await feedSortSelect.askSelection(context, _sort);
@ -123,13 +123,13 @@ class _FeedScreenState extends State<FeedScreen> {
),
feedActionSetType.withProps(
widget.source == FeedSource.domain &&
context.read<SettingsController>().serverSoftware ==
context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy
? ActionLocation.hide
: parseEnum(
ActionLocation.values,
ActionLocation.hide,
context.read<SettingsController>().feedActionSetType.name,
context.watch<SettingsController>().feedActionSetType.name,
),
() async {
final newMode = await feedTypeSelect.askSelection(context, _mode);
@ -149,7 +149,7 @@ class _FeedScreenState extends State<FeedScreen> {
},
),
feedActionRefresh.withProps(
context.read<SettingsController>().feedActionRefresh,
context.watch<SettingsController>().feedActionRefresh,
() {
for (var key in _feedKeyList) {
key.currentState?.refresh();
@ -157,7 +157,7 @@ class _FeedScreenState extends State<FeedScreen> {
},
),
feedActionBackToTop.withProps(
context.read<SettingsController>().feedActionBackToTop,
context.watch<SettingsController>().feedActionBackToTop,
() {
for (var key in _feedKeyList) {
key.currentState?.backToTop();
@ -165,7 +165,7 @@ class _FeedScreenState extends State<FeedScreen> {
},
),
feedActionExpandFab.withProps(
context.read<SettingsController>().feedActionExpandFab,
context.watch<SettingsController>().feedActionExpandFab,
() {
_fabKey.currentState?.toggle();
},
@ -173,12 +173,12 @@ class _FeedScreenState extends State<FeedScreen> {
];
final tabsAction = [
if (context.read<SettingsController>().feedActionSetFilter ==
if (context.watch<SettingsController>().feedActionSetFilter ==
ActionLocationWithTabs.tabs &&
widget.source == null &&
context.read<SettingsController>().isLoggedIn)
context.watch<SettingsController>().isLoggedIn)
actions.firstWhere((action) => action.name == feedActionSetFilter.name),
if (context.read<SettingsController>().feedActionSetType ==
if (context.watch<SettingsController>().feedActionSetType ==
ActionLocationWithTabs.tabs)
actions.firstWhere((action) => action.name == feedActionSetType.name),
].firstOrNull;
@ -194,9 +194,9 @@ class _FeedScreenState extends State<FeedScreen> {
.entries
.firstWhere((entry) =>
entry.value.value ==
(context.read<SettingsController>().serverSoftware !=
(context.watch<SettingsController>().serverSoftware !=
ServerSoftware.lemmy
? context.read<SettingsController>().defaultFeedType
? context.watch<SettingsController>().defaultFeedType
: PostType.thread))
.key,
_ => 0
@ -215,8 +215,8 @@ class _FeedScreenState extends State<FeedScreen> {
contentPadding: EdgeInsets.zero,
title: Text(
widget.title ??
context.read<SettingsController>().selectedAccount +
(context.read<SettingsController>().isLoggedIn
context.watch<SettingsController>().selectedAccount +
(context.watch<SettingsController>().isLoggedIn
? ''
: ' (Guest)'),
maxLines: 1,
@ -534,7 +534,12 @@ class _FeedScreenBodyState extends State<FeedScreenBody> {
@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<FeedScreenBody> {
PagedSliverList(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<PostModel>(
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<FeedScreenBody> {
},
isPreview: item.type == PostType.thread,
),
),
),
);
return context.watch<SettingsController>().postUseCardPreview
? Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
clipBehavior: Clip.antiAlias,
child: inner,
)
: Column(
children: [
inner,
const Divider(height: 1),
],
);
},
),
)
],

View File

@ -44,6 +44,7 @@ class _EntryCommentState extends State<PostComment> {
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<PostComment> {
})
: null,
openLinkUri: Uri.https(
context.read<SettingsController>().instanceHost,
context.read<SettingsController>().serverSoftware ==
context.watch<SettingsController>().instanceHost,
context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy
? '/comment/${widget.comment.id}'
: '/m/${widget.comment.magazine.name}/${switch (widget.comment.postType) {
@ -186,12 +187,12 @@ class _EntryCommentState extends State<PostComment> {
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,
),
),
),

View File

@ -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<SettingsController>().instanceHost,
context.read<SettingsController>().serverSoftware ==
context.watch<SettingsController>().instanceHost,
context.watch<SettingsController>().serverSoftware ==
ServerSoftware.lemmy
? '/post/${item.id}'
: '/m/${item.magazine.name}/${switch (item.type) {

View File

@ -186,10 +186,10 @@ class _PostPageState extends State<PostPage> {
.edit(
post.id,
post.title!,
post.isOc!,
post.isOC!,
body,
post.lang!,
post.isAdult,
post.isNSFW,
),
PostType.microblog => context
.read<SettingsController>()
@ -199,7 +199,7 @@ class _PostPageState extends State<PostPage> {
post.id,
body,
post.lang!,
post.isAdult,
post.isNSFW,
),
};
_onUpdate(newPost);

View File

@ -54,7 +54,7 @@ class MessageItem extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(top: 8),
child: Markdown(item.messages.first.body,
context.read<SettingsController>().instanceHost),
context.watch<SettingsController>().instanceHost),
),
],
),

View File

@ -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<ProfileEditScreen> {
TextEditingController? _aboutTextController;
XFile? _avatarFile;
bool _deleteAvatar = false;
@ -40,7 +38,8 @@ class _ProfileEditScreen extends State<ProfileEditScreen> {
}
void _initSettings() async {
final settings = await context.read<SettingsController>().api.users.getUserSettings();
final settings =
await context.read<SettingsController>().api.users.getUserSettings();
setState(() {
_settings = settings;
});
@ -52,332 +51,354 @@ class _ProfileEditScreen extends State<ProfileEditScreen> {
appBar: AppBar(
actions: [
IconButton(
onPressed: () async {
if (_settingsChanged) {
_settings = await context.read<SettingsController>().api.users.saveUserSettings(_settings!);
}
if (!context.mounted) return;
onPressed: () async {
if (_settingsChanged) {
_settings = await context
.read<SettingsController>()
.api
.users
.saveUserSettings(_settings!);
}
if (!context.mounted) return;
var user = await context.read<SettingsController>().api.users
.updateProfile(_aboutTextController!.text);
var user = await context
.read<SettingsController>()
.api
.users
.updateProfile(_aboutTextController!.text);
if (!context.mounted) return;
if (_deleteAvatar) {
user = await context.read<SettingsController>().api.users
.deleteAvatar();
}
if (!context.mounted) return;
if (_deleteCover) {
user = await context.read<SettingsController>().api.users
.deleteCover();
}
if (!context.mounted) return;
if (_deleteAvatar) {
user = await context
.read<SettingsController>()
.api
.users
.deleteAvatar();
}
if (!context.mounted) return;
if (_deleteCover) {
user = await context
.read<SettingsController>()
.api
.users
.deleteCover();
}
if (!context.mounted) return;
if (_avatarFile != null) {
user = await context.read<SettingsController>().api.users
.updateAvatar(_avatarFile!);
}
if (!context.mounted) return;
if (_coverFile != null) {
user = await context.read<SettingsController>().api.users
.updateCover(_coverFile!);
}
if (!context.mounted) return;
if (!context.mounted) return;
if (_avatarFile != null) {
user = await context
.read<SettingsController>()
.api
.users
.updateAvatar(_avatarFile!);
}
if (!context.mounted) return;
if (_coverFile != null) {
user = await context
.read<SettingsController>()
.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<SettingsController>().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<SettingsController>().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;
});
},
),
],
))
],
),
],
)
),
),
],
)),
);
}
}
}

View File

@ -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<String> themeSelect = SelectionMenu(
.toList(),
);
const SelectionMenu<PostLayout> postLayoutSelect = SelectionMenu(
'Post Layout',
const SelectionMenu<PostImagePosition> 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,
),
],

View File

@ -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<String, dynamic>)
@ -224,144 +223,208 @@ class SettingsController with ChangeNotifier {
notifyListeners();
}
Future<void> updateThemeMode(ThemeMode? newThemeMode) async {
if (newThemeMode == null) return;
if (newThemeMode == _themeMode) return;
_themeMode = newThemeMode;
Future<void> 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<void> updateUseDynamicColor(bool? newUseDynamicColor) async {
if (newUseDynamicColor == null) return;
if (newUseDynamicColor == _useDynamicColor) return;
_useDynamicColor = newUseDynamicColor;
Future<void> 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<void> updateAccentColor(String? newThemeAccent) async {
if (newThemeAccent == null) return;
if (newThemeAccent == _accentColor) return;
Future<void> 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<void> updateAlwaysShowInstance(bool? newShowDisplayInstance) async {
if (newShowDisplayInstance == null) return;
if (newShowDisplayInstance == _alwaysShowInstance) return;
Future<void> 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<void> updatePostLayout(PostLayout? newPostLayout) async {
if (newPostLayout == null) return;
if (newPostLayout == _postLayout) return;
Future<void> 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<void> updateDefaultFeedType(PostType? newDefaultFeedMode) async {
if (newDefaultFeedMode == null) return;
if (newDefaultFeedMode == _defaultFeedType) return;
Future<void> 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<void> updateDefaultEntriesFeedSort(
FeedSort? newDefaultFeedSort) async {
if (newDefaultFeedSort == null) return;
if (newDefaultFeedSort == _defaultEntriesFeedSort) return;
Future<void> 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<void> updateDefaultPostsFeedSort(FeedSort? newDefaultFeedSort) async {
if (newDefaultFeedSort == null) return;
if (newDefaultFeedSort == _defaultPostsFeedSort) return;
Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> addLangFilter(
@ -393,17 +456,17 @@ class SettingsController with ChangeNotifier {
}
Future<void> 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<void> saveServer(ServerSoftware software, String server) async {

View File

@ -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',

48
lib/src/widgets/blur.dart Normal file
View File

@ -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,
),
),
),
],
),
);
}
}

View File

@ -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<ContentItem> {
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<ContentItem> {
: null;
return LayoutBuilder(builder: (context, constrains) {
final isWide = switch (context.watch<SettingsController>().postLayout) {
PostLayout.auto => constrains.maxWidth > 800,
PostLayout.narrow => false,
PostLayout.wide => true,
final hasWideSize = constrains.maxWidth > 800;
final isRightImage =
switch (context.watch<SettingsController>().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<SettingsController>().postLimitTitlePreview
? TextOverflow.ellipsis
: null;
return Column(
children: <Widget>[
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<ContentItem> {
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<ContentItem> {
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (widget.title != null)
Padding(
@ -225,12 +280,9 @@ class _ContentItemState extends State<ContentItem> {
? 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<ContentItem> {
)
: 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<ContentItem> {
),
],
),
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<SettingsController>()
.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<String>(
context: context,
builder: (BuildContext context) =>
AlertDialog(
title: Text(
'Delete ${widget.contentTypeName}'),
actions: <Widget>[
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: <Widget>[
...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<String>(
context: context,
builder: (BuildContext context) =>
AlertDialog(
title: Text(
'Delete ${widget.contentTypeName}'),
actions: <Widget>[
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: <Widget>[
...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: <Widget>[
...commentWidgets,
const Spacer(),
...menuWidgets,
],
),
],
)
: Row(
children: <Widget>[
...commentWidgets,
const Spacer(),
...menuWidgets,
const SizedBox(width: 8),
...votingWidgets,
],
);
}),
),
if (widget.onReply != null &&
_replyTextController != null)
Padding(
@ -574,24 +664,12 @@ class _ContentItemState extends State<ContentItem> {
],
),
),
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,
),
),
],