From 1b164411ea130e04bab9e1fa1e1b67164d730371 Mon Sep 17 00:00:00 2001 From: zoe Date: Sun, 28 Aug 2022 00:29:17 +0200 Subject: [PATCH] fix buttons --- .../interactions/interactions.dart | 59 ++---- .../network_tools/get_post_from_url.dart | 51 +++++ lib/business_logic/notifications/notifs.dart | 2 +- lib/business_logic/timeline/timeline.dart | 24 ++- lib/dialogues/interaction_menu.dart | 17 -- lib/partials/interaction_button.dart | 181 ++++++++++++++---- lib/partials/post.dart | 37 ++-- lib/themes/themes.dart | 2 + 8 files changed, 254 insertions(+), 119 deletions(-) create mode 100644 lib/business_logic/network_tools/get_post_from_url.dart delete mode 100644 lib/dialogues/interaction_menu.dart diff --git a/lib/business_logic/interactions/interactions.dart b/lib/business_logic/interactions/interactions.dart index e4bdd76..1cd4e7a 100644 --- a/lib/business_logic/interactions/interactions.dart +++ b/lib/business_logic/interactions/interactions.dart @@ -1,9 +1,13 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:localization/localization.dart'; +import 'package:loris/business_logic/network_tools/get_post_from_url.dart'; import 'package:loris/global.dart' as global; import 'package:http/http.dart' as http; +import '../timeline/timeline.dart'; + enum InteractionType { favorite, reblog, @@ -45,6 +49,15 @@ extension InteractionTypeExtension on InteractionType { return "unreblog"; } } + + String get name { + switch (this) { + case InteractionType.favorite: + return "like".i18n(); + case InteractionType.reblog: + return "reblog".i18n(); + } + } } Future makeInteractionFromId( @@ -73,48 +86,10 @@ Future makeInteractionFromUrl( InteractionType type, { bool revoke = false, }) async { - Map headers = - global.settings!.identities[id]!.getAuthHeaders(); - headers.addAll(global.defaultHeaders); - - final uriv1 = Uri( - scheme: "https", - host: global.settings!.identities[id]!.instanceUrl, - path: "api/v1/search", - queryParameters: { - "resolve": "true", - "type": "statuses", - "q": posturl, - }, - ); - - http.Response response = await http.get(uriv1, headers: headers); - if (response.statusCode != 200) { - final uriv2 = Uri( - scheme: "https", - host: global.settings!.identities[id]!.instanceUrl, - path: "api/v2/search", - queryParameters: { - "type": "statuses", - "q": posturl, - "resolve": "true", - }, - ); - response = await http.get(uriv2, headers: headers); - } - - if (response.statusCode != 200) { - return response.statusCode; - } - - final Map json = jsonDecode(response.body); - final List statuses = json["statuses"]; - if (statuses.isEmpty) { - return 404; - } - final String postid = statuses[0]["id"]; - - return await makeInteractionFromId(id, postid, type); + Map post = await getPostFromUrl(id, posturl); + return post.keys.first == 200 + ? await makeInteractionFromId(id, post[post.keys.first]!.id, type) + : post.keys.first; } Future makeFullInteraction( diff --git a/lib/business_logic/network_tools/get_post_from_url.dart b/lib/business_logic/network_tools/get_post_from_url.dart new file mode 100644 index 0000000..991ab4b --- /dev/null +++ b/lib/business_logic/network_tools/get_post_from_url.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'package:loris/business_logic/timeline/timeline.dart'; +import 'package:loris/global.dart' as global; +import 'package:http/http.dart' as http; + +Future> getPostFromUrl( + String id, + String posturl, +) async { + Map headers = + global.settings!.identities[id]!.getAuthHeaders(); + headers.addAll(global.defaultHeaders); + + final uriv1 = Uri( + scheme: "https", + host: global.settings!.identities[id]!.instanceUrl, + path: "api/v1/search", + queryParameters: { + "resolve": "true", + "type": "statuses", + "q": posturl, + }, + ); + + http.Response response = await http.get(uriv1, headers: headers); + if (response.statusCode != 200) { + final uriv2 = Uri( + scheme: "https", + host: global.settings!.identities[id]!.instanceUrl, + path: "api/v2/search", + queryParameters: { + "type": "statuses", + "q": posturl, + "resolve": "true", + }, + ); + response = await http.get(uriv2, headers: headers); + } + + if (response.statusCode != 200) { + return {response.statusCode: null}; + } + + final Map json = jsonDecode(response.body); + final List statuses = json["statuses"]; + if (statuses.isEmpty) { + return {404: null}; + } + return {200: PostModel.fromJson(statuses[0], id)}; +} diff --git a/lib/business_logic/notifications/notifs.dart b/lib/business_logic/notifications/notifs.dart index c352319..4573936 100644 --- a/lib/business_logic/notifications/notifs.dart +++ b/lib/business_logic/notifications/notifs.dart @@ -22,7 +22,7 @@ class NotificationModel implements Comparable { type = NotificationType.values.firstWhere( (element) => element.name == "NotificationType.${json["type"]}"); if (json["status"] != null) { - post = PostModel.fromJson(json["status"]); + post = PostModel.fromJson(json["status"], id); } } diff --git a/lib/business_logic/timeline/timeline.dart b/lib/business_logic/timeline/timeline.dart index e3c8b73..13153a7 100644 --- a/lib/business_logic/timeline/timeline.dart +++ b/lib/business_logic/timeline/timeline.dart @@ -12,7 +12,23 @@ class TimelinePartModel { enum Visibility { public, unlisted, private, direct } +extension VisibilityExtension on Visibility { + bool get boostable { + switch (this) { + case Visibility.direct: + return false; + case Visibility.private: + return false; + case Visibility.public: + return true; + case Visibility.unlisted: + return true; + } + } +} + class PostModel implements Comparable { + late String identity; late String id; late String? reblogId; late String createdAt; @@ -29,7 +45,7 @@ class PostModel implements Comparable { // exists if post is a reblog late String originalId; - PostModel.fromJson(Map json) { + PostModel.fromJson(Map json, this.identity) { id = json["id"] as String; reblogId = null; createdAt = json["created_at"]; @@ -41,8 +57,6 @@ class PostModel implements Comparable { } else { rebloggedBy = null; } - - // regular originalId = json["id"]; uri = json["uri"] as String; content = json["content"] as String; @@ -91,7 +105,7 @@ class PostModel implements Comparable { List posts = [this]; int i = 0; while (i < ancestorsJson.length) { - posts.add(PostModel.fromJson(ancestorsJson[i])); + posts.add(PostModel.fromJson(ancestorsJson[i], activeId)); i++; } return ThreadModel(posts); @@ -159,7 +173,7 @@ Future> getTimelineFromServer( final List json = await jsonDecode(response.body); List threads = []; for (int i = 0; i < json.length; i++) { - threads.add(await PostModel.fromJson(json[i]).getThread()); + threads.add(await PostModel.fromJson(json[i], identity).getThread()); } return threads; } diff --git a/lib/dialogues/interaction_menu.dart b/lib/dialogues/interaction_menu.dart deleted file mode 100644 index 8aa6697..0000000 --- a/lib/dialogues/interaction_menu.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:loris/business_logic/interactions/interactions.dart'; - -class InteractionMenu extends StatefulWidget { - const InteractionMenu({required this.type, Key? key}) : super(key: key); - final InteractionType type; - - @override - State createState() => _InteractionMenuState(); -} - -class _InteractionMenuState extends State { - @override - Widget build(BuildContext context) { - return SimpleDialog(); - } -} diff --git a/lib/partials/interaction_button.dart b/lib/partials/interaction_button.dart index 774c0db..74ad7f0 100644 --- a/lib/partials/interaction_button.dart +++ b/lib/partials/interaction_button.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/rendering.dart'; +import 'package:loris/business_logic/network_tools/get_post_from_url.dart'; import 'package:loris/business_logic/timeline/timeline.dart'; import '../business_logic/interactions/interactions.dart' as interactions; import 'package:loris/global.dart' as global; class InteractionButton extends StatefulWidget { - const InteractionButton({required this.model, required this.type, Key? key}) + const InteractionButton( + {required this.model, + required this.type, + Key? key, + this.extended = false}) : super(key: key); final interactions.InteractionType type; final PostModel model; + final bool extended; @override State createState() => _InteractionButtonState(); @@ -19,17 +25,23 @@ class _InteractionButtonState extends State { String idkey = global.settings!.activeIdentity; bool active = false; bool busy = false; + bool success = false; + // user is logged into multiple accounts + + // decides if the button has been clicked already + bool isActive(PostModel post) { + switch (widget.type) { + case interactions.InteractionType.favorite: + return widget.model.favourited; + case interactions.InteractionType.reblog: + return widget.model.reblogged; + } + } @override void initState() { - switch (widget.type) { - case interactions.InteractionType.favorite: - active = widget.model.favourited; - break; - case interactions.InteractionType.reblog: - active = widget.model.reblogged; - break; - } + active = isActive(widget.model); + icon = active ? widget.type.revokeIcon : widget.type.icon; super.initState(); } @@ -39,7 +51,9 @@ class _InteractionButtonState extends State { bool fromUrl = false, }) async { if (!busy) { - busy = true; + setState(() { + busy = true; + }); int status = fromUrl ? await interactions.makeFullInteraction( id, @@ -55,15 +69,19 @@ class _InteractionButtonState extends State { revoke: active, ); + setState(() { + busy = false; + }); + if (status == 200 && mounted) { setState(() { active = !active; icon = active ? widget.type.revokeIcon : widget.type.icon; }); + showSuccess(); } else { await showError(status); } - busy = false; } } @@ -79,45 +97,126 @@ class _InteractionButtonState extends State { }); } + Future> updateIdentities() async { + Map idList = {}; + for (int i = 0; i < global.settings!.identities.length; i++) { + final Map post = await getPostFromUrl( + global.settings!.identities.keys.toList()[i], widget.model.uri); + if (post.keys.first == 200) { + idList.addAll({ + global.settings!.identities.keys.toList()[i]: post[post.keys.first] + }); + } + } + return idList; + } + + Future showSuccess() async { + setState(() { + success = true; + }); + await Future.delayed(const Duration(seconds: 1)); + setState(() { + success = false; + }); + } + @override Widget build(BuildContext context) { - if (global.settings!.identities.length == 1) { + if (!widget.model.visibility.boostable && + widget.type == interactions.InteractionType.reblog) { + return const Icon(Icons.lock); + } + + if (success) { + return const Icon(Icons.check); + } + + if (busy) { + return Center( + heightFactor: 1, + widthFactor: 1, + child: SizedBox( + height: Theme.of(context).iconTheme.size, + width: Theme.of(context).iconTheme.size, + child: const CircularProgressIndicator(), + ), + ); + } + + if (global.settings!.identities.length == 1 || !widget.extended) { return IconButton( + tooltip: widget.type.name, onPressed: () async { - await sendRequest(id: global.settings!.identities.keys.first); + await sendRequest( + id: widget.model.identity, + ); }, icon: Icon(icon), ); } - // user is logged into multiple accounts - List identityPickers = []; - global.settings!.identities.forEach( - (key, value) { - identityPickers.add( - PopupMenuItem( - value: key, - child: Text(key), - ), - ); - }, - ); - - return PopupMenuButton( - onSelected: ((value) { - if (mounted) { + return IconButton( + onPressed: () async { setState(() { - idkey = value as String; + busy = true; }); - } - sendRequest( - id: value as String, - fromUrl: true, - ); - }), - itemBuilder: (context) => identityPickers, - initialValue: idkey, - icon: Icon(icon), - ); + final idList = await updateIdentities(); + setState(() { + busy = false; + }); + await showModalBottomSheet( + context: context, + builder: ((context) { + List c = [ + SelectableText( + widget.type.name, + style: Theme.of(context).textTheme.displayLarge, + ) + ]; + idList.forEach( + (key, value) { + if (value != null) { + c.add( + TextButton.icon( + onPressed: () async { + Navigator.of(context).pop(); + setState(() { + busy = true; + }); + final result = + await interactions.makeInteractionFromId( + key, + value.id, + widget.type, + revoke: isActive(value), + ); + setState(() { + busy = false; + }); + if (result != 200) { + showError(result); + } else { + showSuccess(); + } + }, + icon: Icon( + isActive(value) + ? widget.type.revokeIcon + : widget.type.icon, + ), + label: Text(key), + ), + ); + } + }, + ); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: c, + ); + })); + }, + icon: Icon(icon)); } } diff --git a/lib/partials/post.dart b/lib/partials/post.dart index 75f5516..988a672 100644 --- a/lib/partials/post.dart +++ b/lib/partials/post.dart @@ -250,22 +250,33 @@ class _PostActionBarState extends State { @override Widget build(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - IconButton(onPressed: () {}, icon: const Icon(Icons.reply)), - InteractionButton( - model: widget.model, - type: interactions.InteractionType.reblog, + Expanded( + flex: 20, + child: IconButton(onPressed: () {}, icon: const Icon(Icons.reply))), + Expanded( + flex: 20, + child: InteractionButton( + model: widget.model, + type: interactions.InteractionType.reblog, + ), ), - InteractionButton( - model: widget.model, - type: interactions.InteractionType.favorite, + Expanded( + flex: 20, + child: InteractionButton( + model: widget.model, + type: interactions.InteractionType.favorite, + ), ), - IconButton( - onPressed: () { - popupPostOptions(context, widget.model); - }, - icon: const Icon(Icons.more_horiz), + Expanded( + flex: 20, + child: IconButton( + onPressed: () { + popupPostOptions(context, widget.model); + }, + icon: const Icon(Icons.more_horiz), + ), ) ], ); diff --git a/lib/themes/themes.dart b/lib/themes/themes.dart index 10409df..9da59b1 100644 --- a/lib/themes/themes.dart +++ b/lib/themes/themes.dart @@ -49,6 +49,7 @@ ThemeData getTheme(CustomColors colors) { ), ), iconTheme: IconThemeData( + size: 24, color: colors.colorScheme.onSurface, ), outlinedButtonTheme: OutlinedButtonThemeData( @@ -130,6 +131,7 @@ ThemeData getTheme(CustomColors colors) { selectedRowColor: colors.colorScheme.background, textSelectionTheme: TextSelectionThemeData(selectionColor: colors.hintColor), + primaryIconTheme: const IconThemeData(size: 24), ); }