diff --git a/lib/widgets/profile/components/profile_description.dart b/lib/widgets/profile/components/profile_description.dart new file mode 100644 index 0000000..9f3f639 --- /dev/null +++ b/lib/widgets/profile/components/profile_description.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ProfileDescription extends StatelessWidget { + const ProfileDescription({ + Key? key, + this.htmlData, + }) : super(key: key); + + final String? htmlData; + + @override + Widget build(BuildContext context) { + return Html( + data: htmlData ?? "", + style: { + "p": Style(fontSize: FontSize(16)), + "a": Style( + fontSize: FontSize(16), + textDecoration: TextDecoration.none, + ), + }, + extensions: [ + TagExtension( + tagsToExtend: {"a"}, + builder: (extensionContext) { + return InkWell( + onTap: () => { + launchUrl( + Uri.parse( + extensionContext.element!.attributes["href"]!, + ), + ), + }, + child: Text( + extensionContext.node.text!, + style: const TextStyle( + color: Colors.blue, + fontSize: 16, + ), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/profile/components/profile_name_row.dart b/lib/widgets/profile/components/profile_name_row.dart new file mode 100644 index 0000000..cf4f34c --- /dev/null +++ b/lib/widgets/profile/components/profile_name_row.dart @@ -0,0 +1,120 @@ +import 'package:activitypub/activitypub.dart'; +import 'package:fedodo_general/widgets/profile/enums/profile_button_state.dart'; +import 'package:flutter/material.dart'; + +class ProfileNameRow extends StatefulWidget { + const ProfileNameRow({ + Key? key, + required this.preferredUsername, + required this.userId, + required this.name, + required this.profileButtonInitialState, + }) : super(key: key); + + final String preferredUsername; + final String userId; + final String? name; + final Future profileButtonInitialState; + + @override + State createState() => _ProfileNameRowState(); +} + +class _ProfileNameRowState extends State { + late Future profileButtonState; + + @override + Widget build(BuildContext context) { + + profileButtonState = widget.profileButtonInitialState; + + String fullUserName = + "@${widget.preferredUsername}@${Uri.parse(widget.userId).authority}"; + + return Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name ?? "", + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + Text( + fullUserName, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: Colors.white54, + ), + ), + ], + ), + Column( + children: [ + FutureBuilder( + future: profileButtonState, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + Widget child; + if (snapshot.hasData) { + switch (snapshot.data!) { + case ProfileButtonState.ownProfile: + { + child = ElevatedButton( + onPressed: () {}, + child: const Text("Edit Profile"), + ); + } + break; + case ProfileButtonState.subscribed: + { + child = ElevatedButton( + onPressed: () {}, + child: const Text("Unfollow"), + ); + } + break; + case ProfileButtonState.notSubscribed: + child = ElevatedButton( + onPressed: () async { + ActivityAPI activityApi = ActivityAPI(); + activityApi.follow(Uri.parse(widget.userId)); + + setState(() { + profileButtonState = Future.sync( + () => ProfileButtonState.subscribed); + }); + }, + child: const Text("Follow"), + ); + break; + } + } else if (snapshot.hasError) { + child = const Icon( + Icons.error_outline, + color: Colors.red, + size: 60, + ); + } else { + child = ElevatedButton( + onPressed: () {}, + child: const Text("Loading"), + ); + } + return child; + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/profile/components/profile_picture_detail.dart b/lib/widgets/profile/components/profile_picture_detail.dart new file mode 100644 index 0000000..0faad43 --- /dev/null +++ b/lib/widgets/profile/components/profile_picture_detail.dart @@ -0,0 +1,91 @@ +import 'package:fedodo_general/Extensions/string_extensions.dart'; +import 'package:flutter/material.dart'; + +class ProfilePictureDetail extends StatefulWidget { + const ProfilePictureDetail({ + Key? key, + required this.followersCount, + required this.followingCount, + required this.postsCount, + this.iconUrl, + }) : super(key: key); + + final int followersCount; + final int followingCount; + final int postsCount; + final String? iconUrl; + + @override + State createState() => _ProfilePictureDetailState(); +} + +class _ProfilePictureDetailState extends State { + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: Image.network( + width: 80, + height: 80, + widget.iconUrl != null + ? widget.iconUrl!.asFedodoProxyString() + : "https://upload.wikimedia.org/wikipedia/commons/8/89/Portrait_Placeholder.png?20170328184010" + .asFedodoProxyString(), + ), + ), + ], + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + Text( + widget.postsCount.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Text("Posts"), + ], + ), + Column( + children: [ + Text( + widget.followingCount.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Text("Following"), + ], + ), + Column( + children: [ + Text( + widget.followersCount.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Text("Followers"), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/profile/enums/profile_button_state.dart b/lib/widgets/profile/enums/profile_button_state.dart new file mode 100644 index 0000000..a623209 --- /dev/null +++ b/lib/widgets/profile/enums/profile_button_state.dart @@ -0,0 +1,5 @@ +enum ProfileButtonState { + ownProfile, + subscribed, + notSubscribed +} \ No newline at end of file diff --git a/lib/widgets/profile/profile_head.dart b/lib/widgets/profile/profile_head.dart new file mode 100644 index 0000000..a7f222a --- /dev/null +++ b/lib/widgets/profile/profile_head.dart @@ -0,0 +1,110 @@ +import 'package:activitypub/APIs/followers_api.dart'; +import 'package:activitypub/APIs/followings_api.dart'; +import 'package:activitypub/APIs/outbox_api.dart'; +import 'package:activitypub/Models/actor.dart'; +import 'package:activitypub/Models/ordered_paged_collection.dart'; +import 'package:fedodo_general/Globals/general.dart'; +import 'package:flutter/material.dart'; +import 'components/profile_description.dart'; +import 'components/profile_name_row.dart'; +import 'components/profile_picture_detail.dart'; +import 'enums/profile_button_state.dart'; + +class ProfileHead extends StatefulWidget { + const ProfileHead({ + super.key, + required this.actor, + }); + + final Actor actor; + + @override + State createState() => _ProfileHeadState(); +} + +class _ProfileHeadState extends State { + int? postCount; + int? followingCount; + int? followersCount; + + @override + Widget build(BuildContext context) { + if (followersCount == null && widget.actor.followers != null) { + setFollowers(widget.actor.followers!); + } + if (followingCount == null && widget.actor.following != null) { + setFollowings(widget.actor.following!); + } + if (postCount == null) { + setPosts(widget.actor.outbox!); + } + + return Column( + children: [ + ProfilePictureDetail( + followersCount: followersCount ?? 0, + followingCount: followingCount ?? 0, + iconUrl: widget.actor.icon?.url, + postsCount: postCount ?? 0, + ), + ProfileNameRow( + profileButtonInitialState: getProfileButtonState(widget.actor), + preferredUsername: widget.actor.preferredUsername!, + userId: widget.actor.id!, + name: widget.actor.name, + ), + ProfileDescription( + htmlData: widget.actor.summary ?? "", + ), + ], + ); + } + + Future getProfileButtonState(Actor actor) async { + if (actor.id == null) General.logger.w("ActorId was null!"); + + if (actor.id == General.fullActorId) { + return ProfileButtonState.ownProfile; + } else { + FollowingsAPI followingsAPI = FollowingsAPI(); + var isFollowed = + await followingsAPI.isFollowed(actor.id!, General.fullActorId); + if (isFollowed) { + return ProfileButtonState.subscribed; + } else { + return ProfileButtonState.notSubscribed; + } + } + } + + void setFollowers(String followersString) async { + FollowersAPI followersApi = FollowersAPI(); + OrderedPagedCollection followersCollection = + await followersApi.getFollowers(followersString); + + setState(() { + followersCount = followersCollection.totalItems; + }); + } + + void setFollowings(String followingsString) async { + FollowingsAPI followersProvider = FollowingsAPI(); + OrderedPagedCollection followingCollection = + await followersProvider.getFollowings(followingsString); + + setState(() { + followingCount = followingCollection.totalItems; + }); + } + + void setPosts(String outboxUrl) async { + OutboxAPI outboxProvider = OutboxAPI(); + + OrderedPagedCollection orderedPagedCollection = + await outboxProvider.getFirstPage(outboxUrl); + + setState(() { + postCount = orderedPagedCollection.totalItems; + }); + } +} diff --git a/lib/Widgets/search.dart b/lib/widgets/search.dart similarity index 100% rename from lib/Widgets/search.dart rename to lib/widgets/search.dart diff --git a/pubspec.yaml b/pubspec.yaml index dd6663c..78bb838 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fedodo_general description: A flutter package which contains general components of the Fedodo-UIs. -version: 1.6.5 +version: 1.7.0 homepage: https://fedodo.org environment: @@ -20,6 +20,7 @@ dependencies: jwt_decoder: ^2.0.1 easy_sidemenu: ^0.4.1+1 logger: ^1.4.0 + flutter_html: ^3.0.0-beta.1 dev_dependencies: flutter_test: