following n stuff

This commit is contained in:
zoe 2022-09-05 19:01:04 +02:00
parent effd78f758
commit 2b0671d785
6 changed files with 403 additions and 72 deletions

View File

@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:loris/business_logic/timeline/timeline.dart';
import 'package:loris/global.dart' as global;
class AccountModel {
@ -54,6 +56,7 @@ class AccountModel {
}
class RelationshipModel {
final String identity;
final bool blockedBy;
final bool blocking;
final bool endorsed;
@ -68,6 +71,7 @@ class RelationshipModel {
final bool showingReblogs;
RelationshipModel({
required this.identity,
required this.blockedBy,
required this.blocking,
required this.endorsed,
@ -82,8 +86,12 @@ class RelationshipModel {
required this.showingReblogs,
});
factory RelationshipModel.fromJson(Map<String, dynamic> json) {
factory RelationshipModel.fromJson(
Map<String, dynamic> json,
String identity,
) {
return RelationshipModel(
identity: identity,
blockedBy: json["blocked_by"],
blocking: json["blocking"],
endorsed: json["endorsed"],
@ -117,7 +125,7 @@ Future<Map<int, RelationshipModel?>> getRelationship(
if (response.statusCode == 200) {
return {
response.statusCode:
RelationshipModel.fromJson(jsonDecode(response.body)[0])
RelationshipModel.fromJson(jsonDecode(response.body)[0], identityName)
};
}
@ -134,6 +142,8 @@ Future<Map<int, AccountModel?>> searchModel(
Map<String, String> params = {
"type": "accounts",
"q": url,
"limit": "1",
"resolve": "true",
};
final uri1 = Uri(
@ -150,7 +160,6 @@ Future<Map<int, AccountModel?>> searchModel(
);
final r1 = await http.get(uri1, headers: headers);
if (r1.statusCode == 200) {
List<dynamic> accounts = jsonDecode(r1.body)["accounts"];
if (accounts.isEmpty) {
@ -179,3 +188,115 @@ Future<Map<int, AccountModel?>> searchModel(
}
return {r2.statusCode: null};
}
Future<MapEntry<int, List<ThreadModel>?>> getPostsForAccount(
AccountModel model,
String? maxid,
) async {
final identity = global.settings!.identities[model.identity];
var headers = identity!.getAuthHeaders();
headers.addAll(global.defaultHeaders);
final params = {
"limit": global.settings!.batchSize.toString(),
if (maxid != null) "maxid": maxid,
};
final uri = Uri(
host: identity.instanceUrl,
path: "/api/v1/accounts/${model.id}/statuses",
queryParameters: params,
);
final r = await http.get(uri, headers: headers);
if (r.statusCode != 200) {
return MapEntry(r.statusCode, null);
}
final List<Map<String, dynamic>> rb = jsonDecode(r.body);
List<ThreadModel> threads = [];
// ignore: avoid_function_literals_in_foreach_calls
rb.forEach((element) async {
threads.add(
await PostModel.fromJson(
element,
model.identity,
).getThread(),
);
});
return MapEntry(r.statusCode, threads);
}
enum AccountInteractionTypes {
follow,
block,
mute,
}
extension AccountInteractionTypesExenstion on AccountInteractionTypes {
String get slug {
switch (this) {
case AccountInteractionTypes.block:
return "block";
case AccountInteractionTypes.follow:
return "follow";
case AccountInteractionTypes.mute:
return "mute";
}
}
IconData get icon {
switch (this) {
case AccountInteractionTypes.block:
return Icons.block;
case AccountInteractionTypes.follow:
return Icons.person_add;
case AccountInteractionTypes.mute:
return Icons.volume_off;
}
}
String get revokeSlug {
switch (this) {
case AccountInteractionTypes.block:
return "unblock";
case AccountInteractionTypes.follow:
return "unfollow";
case AccountInteractionTypes.mute:
return "unmute";
}
}
}
enum AccountTextInteractionTypes {
note,
report,
}
Future<MapEntry<int, RelationshipModel?>> performInteraction(
String identityName,
String accountId,
AccountInteractionTypes type,
bool revoke,
) async {
final identity = global.settings!.identities[identityName]!;
var headers = identity.getAuthHeaders();
headers.addAll(global.defaultHeaders);
headers.remove("Content-Type");
final uri = Uri(
scheme: "https",
host: identity.instanceUrl,
path:
"/api/v1/accounts/$accountId/${revoke ? type.revokeSlug : type.slug}");
final response = await http.post(uri, headers: headers);
if (response.statusCode != 200) {
return MapEntry(response.statusCode, null);
}
return MapEntry(
response.statusCode,
RelationshipModel.fromJson(
jsonDecode(response.body),
identityName,
),
);
}

View File

@ -68,6 +68,9 @@
"you-are-mufos": "you are mutuals",
"they-follow-you": "they follow you",
"you-follow-them": "you follow them",
"you-do-not-follow-each-other": "you don't follow each other"
"you-do-not-follow-each-other": "you don't follow each other",
"found-account-on": "found account on",
"instances": "instances",
"instance": "instace"
}

View File

@ -1,6 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:localization/localization.dart';
import 'package:loris/business_logic/account/account.dart';
import 'package:loris/business_logic/settings.dart';
import 'package:loris/partials/post.dart';
import 'package:loris/partials/post_text_renderer.dart';
import 'package:loris/global.dart' as global;
@ -11,72 +14,175 @@ class ProfileView extends StatefulWidget {
required this.model,
});
final AccountModel model;
@override
State<ProfileView> createState() => _ProfileViewState();
}
class _ProfileViewState extends State<ProfileView> {
final StreamController<MapEntry<String, RelationshipModel>>
_relationshipStream = StreamController();
Map<String, AccountModel> identities = {};
Map<String, RelationshipModel?> relationships = {};
String activeIdentity = "";
bool loading = true;
void update() async {
for (var element in global.settings!.identities.keys) {
final m = await searchModel(element, widget.model.url);
if (m.values.first != null) {
Future<void> addRelationship(AccountModel m) async {
final r = await getRelationship(m.identity, m.id);
if (r.keys.first != 200) {
return;
} else if (mounted) {
setState(() {
relationships.addAll({
m.identity: r.values.first,
});
});
}
}
void addIdentity(String identityName, int i) async {
final m = await searchModel(identityName, widget.model.url);
if (m.values.first != null) {
if (mounted) {
setState(() {
identities.addAll({element: m.values.first!});
identities.addAll({identityName: m.values.first!});
});
}
await addRelationship(m.values.first!);
}
if (i >= global.settings!.identities.length) {
setState(() {
loading = false;
});
}
}
void update() async {
int i = 0;
await Future.forEach<MapEntry<String, AccountSettings>>(
global.settings!.identities.entries,
(element) async {
if (element.key == widget.model.identity) {
i++;
addIdentity(element.key, i);
return;
}
i++;
addIdentity(element.key, i);
},
);
}
@override
void dispose() {
_relationshipStream.close();
super.dispose();
}
@override
void initState() {
activeIdentity = widget.model.identity;
identities.addAll({widget.model.identity: widget.model});
super.initState();
update();
_relationshipStream.stream.listen((event) {
setState(() {
relationships.addEntries([event]);
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
List<DropdownMenuItem<String>> dmenuItems = [];
identities.forEach((key, value) {
dmenuItems.add(DropdownMenuItem(
alignment: Alignment.center,
value: key,
child: Text(
key,
style: Theme.of(context).textTheme.bodyMedium,
),
));
});
return SimpleDialog(
contentPadding: const EdgeInsets.all(24),
children: [
DropdownButtonHideUnderline(
child: DropdownButton(
isExpanded: false,
alignment: Alignment.center,
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
value: activeIdentity,
items: dmenuItems,
onChanged: (value) {
setState(() {
activeIdentity = value.toString();
});
},
dmenuItems.add(
DropdownMenuItem(
alignment: Alignment.center,
value: key,
child: Text(
key,
style: Theme.of(context).textTheme.bodyMedium,
),
),
);
});
return SimpleDialog(
alignment: Alignment.center,
title: DisplayName(
account: identities[activeIdentity]!,
openInBrowser: true,
),
contentPadding: const EdgeInsets.all(24),
children: [
loading
? Column(
children: [
SelectableText(
"${"found-account-on".i18n()} ${identities.length} ${identities.length <= 1 ? "instance" : "instances".i18n()}"),
const LinearProgressIndicator(),
],
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DropdownButtonHideUnderline(
child: DropdownButton(
isExpanded: false,
alignment: Alignment.center,
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
value: activeIdentity,
items: dmenuItems,
onChanged: (value) {
setState(() {
activeIdentity = value.toString();
});
},
),
),
],
),
ProfileViewDisplay(
model: identities[activeIdentity]!,
accountModel: identities[activeIdentity]!,
relationshipModel: relationships[activeIdentity],
stream: _relationshipStream,
)
],
);
}
}
class AccountInteractionButtons extends StatelessWidget {
const AccountInteractionButtons({
super.key,
required this.account,
required this.relationship,
required this.stream,
});
final AccountModel account;
final RelationshipModel relationship;
final StreamController stream;
@override
Widget build(BuildContext context) {
return Wrap(
children: [
AccountInteractionButton(
accountModel: account,
relationshipModel: relationship,
type: AccountInteractionTypes.follow,
stream: stream,
),
AccountInteractionButton(
accountModel: account,
relationshipModel: relationship,
type: AccountInteractionTypes.block,
stream: stream,
),
],
);
}
}
class StatusIndicators extends StatelessWidget {
const StatusIndicators({
super.key,
@ -136,55 +242,46 @@ class StatusIndicators extends StatelessWidget {
}
}
class ProfileViewDisplay extends StatefulWidget {
const ProfileViewDisplay({super.key, required this.model});
final AccountModel model;
@override
State<ProfileViewDisplay> createState() => _ProfileViewDisplayState();
}
class _ProfileViewDisplayState extends State<ProfileViewDisplay> {
RelationshipModel? relationship;
class ProfileViewDisplay extends StatelessWidget {
const ProfileViewDisplay({
super.key,
required this.accountModel,
this.relationshipModel,
required this.stream,
});
final AccountModel accountModel;
final RelationshipModel? relationshipModel;
final StreamController stream;
static const d = SizedBox(
height: 8,
);
void update() async {
final r = await getRelationship(widget.model.identity, widget.model.id);
setState(() {
relationship = r.values.first;
});
}
@override
void initState() {
super.initState();
update();
}
@override
Widget build(BuildContext context) {
List<Widget> c = [
Image.network(
width: global.getWidth(context),
fit: BoxFit.fitWidth,
widget.model.header,
accountModel.header,
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
),
StatusIndicators(
model: widget.model,
relationship: relationship,
model: accountModel,
relationship: relationshipModel,
),
d,
DisplayName(
account: widget.model,
clickable: false,
PostTextRenderer(input: accountModel.note),
if (relationshipModel != null)
AccountInteractionButtons(
account: accountModel,
relationship: relationshipModel!,
stream: stream,
),
AccountPostList(
accountModel: accountModel,
),
d,
PostTextRenderer(input: widget.model.note),
];
return Container(
constraints: global.getConstraints(context),
width: global.getWidth(context),
@ -195,3 +292,80 @@ class _ProfileViewDisplayState extends State<ProfileViewDisplay> {
);
}
}
class AccountPostList extends StatefulWidget {
const AccountPostList({
super.key,
required this.accountModel,
});
final AccountModel accountModel;
@override
State<AccountPostList> createState() => _AccountPostListState();
}
class _AccountPostListState extends State<AccountPostList> {
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height,
),
child: ListView(),
);
}
}
class AccountInteractionButton extends StatefulWidget {
const AccountInteractionButton({
super.key,
required this.type,
required this.relationshipModel,
required this.accountModel,
required this.stream,
});
final AccountInteractionTypes type;
final RelationshipModel relationshipModel;
final AccountModel accountModel;
final StreamController stream;
@override
State<AccountInteractionButton> createState() =>
AccountInteractionButtonState();
}
class AccountInteractionButtonState extends State<AccountInteractionButton> {
bool active() {
switch (widget.type) {
case AccountInteractionTypes.follow:
return widget.relationshipModel.following;
case AccountInteractionTypes.block:
return widget.relationshipModel.blocking;
case AccountInteractionTypes.mute:
return widget.relationshipModel.muting;
}
}
@override
Widget build(BuildContext context) {
return TextButton.icon(
onPressed: () async {
final r = await performInteraction(
widget.relationshipModel.identity,
widget.accountModel.id,
widget.type,
active(),
);
if (r.value == null) {
return;
}
widget.stream
.add(MapEntry(widget.relationshipModel.identity, r.value!));
},
icon: Icon(widget.type.icon),
label: Text(
active() ? widget.type.revokeSlug.i18n() : widget.type.slug.i18n(),
),
);
}
}

View File

@ -8,6 +8,7 @@ import 'package:loris/partials/interaction_button.dart';
import 'package:loris/partials/media_attachment.dart';
import 'package:loris/partials/post_options.dart';
import 'package:loris/partials/post_text_renderer.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../business_logic/timeline/timeline.dart' as tl;
import '../business_logic/interactions/interactions.dart' as interactions;
@ -65,12 +66,12 @@ class DisplayName extends StatelessWidget {
const DisplayName({
required this.account,
this.isReblog = false,
this.clickable = true,
this.openInBrowser = false,
Key? key,
}) : super(key: key);
final AccountModel account;
final bool isReblog;
final bool clickable;
final bool openInBrowser;
@override
Widget build(BuildContext context) {
@ -81,14 +82,14 @@ class DisplayName extends StatelessWidget {
usernameStyle = Theme.of(context).textTheme.displaySmall;
}
return InkWell(
onTap: clickable
onTap: !openInBrowser
? () {
showDialog(
context: context,
builder: (context) => ProfileView(model: account),
);
}
: null,
: (() => launchUrlString(account.url)),
child: Row(
children: [
ProfilePic(url: account.avatar),

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'themes.dart' as themes;
themes.CustomColors theme = themes.CustomColors(
"website #4",
const Color.fromARGB(
255,
255,
170,
90,
),
const ColorScheme(
brightness: Brightness.light,
primary: Color.fromARGB(255, 131, 37, 79),
onPrimary: Color.fromARGB(255, 255, 249, 242),
secondary: Color.fromARGB(255, 81, 17, 46),
onSecondary: Color.fromARGB(255, 255, 249, 242),
error: Color.fromARGB(255, 255, 85, 85),
onError: Color.fromARGB(255, 255, 249, 242),
background: Color.fromARGB(255, 255, 241, 223),
onBackground: Color.fromARGB(255, 248, 248, 242),
surface: Color.fromARGB(255, 255, 249, 242),
onSurface: Color.fromARGB(255, 25, 25, 25),
));

View File

@ -3,6 +3,7 @@ import 'dracula.dart' as color_dracula;
import 'tess.dart' as color_tess;
import 'adwaita.dart' as color_adwaita;
import 'gruvbox.dart' as color_gruvbox;
import 'fourth_website.dart' as color_fourth;
// color schemes to pick from can be added here
// there is a class to create these
@ -13,6 +14,7 @@ final available = [
color_tess.theme,
color_gruvbox.themeDark,
color_gruvbox.themeLight,
color_fourth.theme,
];
ThemeData getTheme(CustomColors colors) {
@ -172,6 +174,12 @@ ThemeData getTheme(CustomColors colors) {
),
),
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: colors.colorScheme.primary,
refreshBackgroundColor: colors.hintColor,
linearTrackColor: colors.hintColor,
circularTrackColor: colors.hintColor,
),
);
}