following n stuff
This commit is contained in:
parent
effd78f758
commit
2b0671d785
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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),
|
||||
));
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue