fix buttons

This commit is contained in:
zoe 2022-08-28 00:29:17 +02:00
parent 2c2e66d842
commit 1b164411ea
8 changed files with 254 additions and 119 deletions

View File

@ -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<int> makeInteractionFromId(
@ -73,48 +86,10 @@ Future<int> makeInteractionFromUrl(
InteractionType type, {
bool revoke = false,
}) async {
Map<String, String> 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<String, dynamic> json = jsonDecode(response.body);
final List<dynamic> statuses = json["statuses"];
if (statuses.isEmpty) {
return 404;
}
final String postid = statuses[0]["id"];
return await makeInteractionFromId(id, postid, type);
Map<int, PostModel?> post = await getPostFromUrl(id, posturl);
return post.keys.first == 200
? await makeInteractionFromId(id, post[post.keys.first]!.id, type)
: post.keys.first;
}
Future<int> makeFullInteraction(

View File

@ -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<Map<int, PostModel?>> getPostFromUrl(
String id,
String posturl,
) async {
Map<String, String> 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<String, dynamic> json = jsonDecode(response.body);
final List<dynamic> statuses = json["statuses"];
if (statuses.isEmpty) {
return {404: null};
}
return {200: PostModel.fromJson(statuses[0], id)};
}

View File

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

View File

@ -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<String, dynamic> json) {
PostModel.fromJson(Map<String, dynamic> 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<PostModel> 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<List<ThreadModel>> getTimelineFromServer(
final List<dynamic> json = await jsonDecode(response.body);
List<ThreadModel> 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;
}

View File

@ -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<InteractionMenu> createState() => _InteractionMenuState();
}
class _InteractionMenuState extends State<InteractionMenu> {
@override
Widget build(BuildContext context) {
return SimpleDialog();
}
}

View File

@ -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<InteractionButton> createState() => _InteractionButtonState();
@ -19,17 +25,23 @@ class _InteractionButtonState extends State<InteractionButton> {
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<InteractionButton> {
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<InteractionButton> {
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<InteractionButton> {
});
}
Future<Map<String, PostModel?>> updateIdentities() async {
Map<String, PostModel?> idList = {};
for (int i = 0; i < global.settings!.identities.length; i++) {
final Map<int, PostModel?> 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<void> 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<PopupMenuItem> 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<Widget> 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));
}
}

View File

@ -250,22 +250,33 @@ class _PostActionBarState extends State<PostActionBar> {
@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),
),
)
],
);

View File

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