we have posting!

This commit is contained in:
zoe 2022-08-29 23:16:13 +02:00
parent cb81269576
commit 116d7e6c78
9 changed files with 462 additions and 78 deletions

View File

@ -0,0 +1,59 @@
import 'dart:convert';
import 'package:http/http.dart';
import 'package:loris/global.dart' as global;
class InstanceInformation {
final InstanceConfiguration configuration;
InstanceInformation(this.configuration);
static InstanceInformation fromJson(Map<String, dynamic> json) {
return InstanceInformation(
InstanceConfiguration.fromJson(
json["configuration"],
),
);
}
}
class InstanceConfiguration {
final StatusConfiguration statusconfig;
InstanceConfiguration(this.statusconfig);
static InstanceConfiguration fromJson(Map<String, dynamic> json) {
return InstanceConfiguration(
StatusConfiguration.fromJson(json["statuses"]),
);
}
}
class StatusConfiguration {
final int maxChars;
StatusConfiguration(this.maxChars);
static StatusConfiguration fromJson(Map<String, dynamic> json) {
return StatusConfiguration(
json["max_characters"],
);
}
}
Future<Map<int, InstanceInformation?>> instanceInformationForIdentity(
String id,
) async {
final Uri url = Uri(
scheme: "https",
host: global.settings!.identities[id]!.instanceUrl,
path: "/api/v1/instance",
);
final response = await get(
url,
headers: global.defaultHeaders,
);
if (response.statusCode != 200) {
return {response.statusCode: null};
}
return {
response.statusCode: InstanceInformation.fromJson(jsonDecode(response.body))
};
}

View File

@ -0,0 +1,62 @@
import 'dart:convert';
import 'package:loris/business_logic/timeline/timeline.dart';
import 'package:http/http.dart' as http;
import 'package:loris/global.dart' as global;
class MakePostModel {
final String identity;
final String status;
final String? spoilerText;
final Visibility visibility;
final String? scheduledAt;
final String? inReplyToId;
MakePostModel({
required this.identity,
required this.status,
this.spoilerText,
required this.visibility,
this.scheduledAt,
this.inReplyToId,
});
Future<int> sendPost() async {
final headers = global.settings!.identities[identity]!.getAuthHeaders();
headers.addAll(global.defaultHeaders);
Map<String, dynamic> params = {
"status": status,
"sensitive": false,
"visibility": visibility.queryParam,
};
if (inReplyToId != null) {
params.addAll({
"in_reply_to_id": inReplyToId,
});
}
if (spoilerText != null) {
if (spoilerText!.isNotEmpty) {
params.addAll({
"spoiler_text": spoilerText!,
"sensitive": true,
});
}
}
final url = Uri(
scheme: "https",
host: global.settings!.identities[identity]!.instanceUrl,
path: "/api/v1/statuses",
);
final response = await http.post(
url,
headers: headers,
body: jsonEncode(params),
);
return response.statusCode;
}
}

View File

@ -41,6 +41,19 @@ extension VisibilityExtension on Visibility {
}
}
String get queryParam {
switch (this) {
case Visibility.direct:
return "direct";
case Visibility.private:
return "private";
case Visibility.public:
return "public";
case Visibility.unlisted:
return "unlisted";
}
}
IconData get icon {
switch (this) {
case Visibility.direct:

View File

@ -1,26 +1,226 @@
import 'package:flutter/material.dart';
import 'package:localization/localization.dart';
import 'package:loris/business_logic/timeline/timeline.dart';
import '../business_logic/posting/posting.dart' as logic;
import 'package:loris/business_logic/instance/instance.dart';
import 'package:loris/business_logic/network_tools/get_post_from_url.dart';
import 'package:loris/business_logic/timeline/timeline.dart' as tl;
import '../business_logic/posting/posting.dart';
import '../partials/post.dart';
import 'package:loris/global.dart' as global;
class MakePost extends StatefulWidget {
const MakePost({Key? key, this.inReplyTo}) : super(key: key);
final PostModel? inReplyTo;
final tl.PostModel? inReplyTo;
@override
State<MakePost> createState() => _MakePostState();
}
class _MakePostState extends State<MakePost> {
String accountid = global.settings!.activeIdentity;
int? maxLength;
String text = "";
String spoilerText = "";
// stores all identities and if available the post id you are replying to
Map<String, String?> identitiesAvailable = {};
tl.Visibility visibility = tl.Visibility.public;
void switchAccount(String acct) {
setState(() {
maxLength = null;
accountid = acct;
});
updateMaxChars();
}
void updateMaxChars() async {
final info = await instanceInformationForIdentity(accountid);
if (info.keys.first == 200) {
setState(() {
maxLength = info.values.first!.configuration.statusconfig.maxChars;
});
}
}
void updateAvailableIds() async {
if (widget.inReplyTo != null) {
global.settings!.identities.forEach((key, value) async {
final post = await getPostFromUrl(key, widget.inReplyTo!.uri);
if (post.keys.first == 200) {
setState(() {
identitiesAvailable.addAll({key: post.values.first!.id});
});
}
});
}
}
@override
void initState() {
if (widget.inReplyTo != null) {
accountid = widget.inReplyTo!.identity;
identitiesAvailable.addAll({
widget.inReplyTo!.identity: widget.inReplyTo!.id,
});
} else {
global.settings!.identities.forEach((key, value) {
if (!identitiesAvailable.containsKey(DropdownMenuItem(
value: key,
child: Text(key),
))) {
identitiesAvailable.addAll({key: null});
}
});
}
super.initState();
updateMaxChars();
updateAvailableIds();
}
@override
Widget build(BuildContext context) {
List<Widget> c = [];
if (widget.inReplyTo != null) {
c.add(
Post(
model: widget.inReplyTo!,
reblogVisible: false,
hideActionBar: true,
),
);
}
List<DropdownMenuItem<String>> idDropdownItems = [];
identitiesAvailable.forEach((key, value) {
idDropdownItems.add(DropdownMenuItem(
value: key,
child: Text(key),
));
});
List<DropdownMenuItem<tl.Visibility>> visibilityDropdowns = [];
for (var value in tl.Visibility.values) {
visibilityDropdowns.add(DropdownMenuItem(
value: value,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(value.icon),
Text(value.name),
],
)));
}
List<Widget> actionButtons = [
maxLength == null
? const CircularProgressIndicator()
: SelectableText((maxLength! - text.length).toString()),
DropdownButtonHideUnderline(
child: DropdownButton<tl.Visibility>(
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
items: visibilityDropdowns,
value: visibility,
onChanged: (value) {
setState(() {
visibility = value ?? tl.Visibility.private;
});
},
),
),
DropdownButtonHideUnderline(
child: DropdownButton<String>(
borderRadius: BorderRadius.circular(8),
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
items: idDropdownItems,
value: accountid,
onChanged: (value) {
switchAccount(value as String);
},
),
),
OutlinedButton.icon(
label: Text(
"send-post".i18n(),
),
// send the post!!!
onPressed: () async {
final model = MakePostModel(
identity: accountid,
status: text,
visibility: visibility,
inReplyToId: widget.inReplyTo == null
? null
: identitiesAvailable[accountid]!,
);
model.sendPost();
Navigator.of(context).pop();
},
icon: const Icon(Icons.send),
),
];
c.addAll([
TextFormField(
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
prefixIcon: Icon(
Icons.warning,
color: Theme.of(context).colorScheme.onSurface,
),
labelText: "content-warning".i18n(),
),
onChanged: ((value) => setState(() {
spoilerText = value;
})),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
child: TextFormField(
style: Theme.of(context).textTheme.bodyMedium,
maxLines: null,
expands: true,
onChanged: (value) {
setState(() {
text = value;
});
},
),
),
const SizedBox(
height: 24,
),
Wrap(
runSpacing: 8,
spacing: 24,
direction: Axis.horizontal,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceAround,
children: actionButtons,
),
]);
return SimpleDialog(
alignment: Alignment.center,
contentPadding: const EdgeInsets.all(24),
title: SelectableText(
textAlign: TextAlign.center,
widget.inReplyTo == null ? "make-post".i18n() : "make-reply".i18n(),
style: Theme.of(context).textTheme.displayMedium),
children: [],
children: [
SingleChildScrollView(
child: Container(
width: (MediaQuery.of(context).size.width *
global.settings!.postWidth) -
56,
constraints: BoxConstraints(
maxWidth: global.settings!.maxPostWidth,
minWidth: 375,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: c,
),
),
),
],
);
}
}

View File

@ -49,6 +49,7 @@
"private-visibility": "private",
"direct-visibility": "dm",
"make-reply": "make reply",
"make-post": "make post"
"make-post": "make post",
"content-warning": "content warning"
}

View File

@ -98,11 +98,12 @@ class TimelineState extends State<Timeline> {
}
selectedId = global.settings!.activeIdentity;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
child: Wrap(
alignment: WrapAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
@ -110,66 +111,74 @@ class TimelineState extends State<Timeline> {
},
icon: const Icon(Icons.refresh),
),
DropdownButton(
value: selectedId,
items: identities,
onChanged: (dynamic value) async {
setState(() {
selectedId = value ?? global.settings!.activeIdentity;
global.settings!.saveActiveIdentity(selectedId);
reload();
});
},
DropdownButtonHideUnderline(
child: DropdownButton(
borderRadius: BorderRadius.circular(8),
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
value: selectedId,
items: identities,
onChanged: (dynamic value) async {
setState(() {
selectedId = value ?? global.settings!.activeIdentity;
global.settings!.saveActiveIdentity(selectedId);
reload();
});
},
),
),
DropdownButton(
value: selectedTimelineType,
items: [
DropdownMenuItem(
value: tl.TimelineType.home,
child: RichText(
text: TextSpan(
text: "${"home-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.home),
),
],
DropdownButtonHideUnderline(
child: DropdownButton(
borderRadius: BorderRadius.circular(8),
iconEnabledColor: Theme.of(context).colorScheme.onSurface,
value: selectedTimelineType,
items: [
DropdownMenuItem(
value: tl.TimelineType.home,
child: RichText(
text: TextSpan(
text: "${"home-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.home),
),
],
),
),
),
),
DropdownMenuItem(
value: tl.TimelineType.local,
child: RichText(
text: TextSpan(
text: "${"local-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.people),
)
],
DropdownMenuItem(
value: tl.TimelineType.local,
child: RichText(
text: TextSpan(
text: "${"local-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.people),
)
],
),
),
),
),
DropdownMenuItem(
value: tl.TimelineType.public,
child: RichText(
text: TextSpan(
text: "${"public-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.public),
),
],
DropdownMenuItem(
value: tl.TimelineType.public,
child: RichText(
text: TextSpan(
text: "${"public-timeline".i18n()} ",
children: const [
WidgetSpan(
child: Icon(Icons.public),
),
],
),
),
),
),
],
onChanged: (tl.TimelineType? value) {
setState(() {
selectedTimelineType = value ?? tl.TimelineType.home;
reload();
});
},
],
onChanged: (tl.TimelineType? value) {
setState(() {
selectedTimelineType = value ?? tl.TimelineType.home;
reload();
});
},
),
)
],
),

View File

@ -16,10 +16,12 @@ class Post extends StatefulWidget {
Key? key,
this.reblogVisible = true,
this.hideSensitive = true,
this.hideActionBar = false,
}) : super(key: key);
final tl.PostModel model;
final bool reblogVisible;
final bool hideSensitive;
final bool hideActionBar;
@override
State<Post> createState() => _PostState();
@ -28,24 +30,32 @@ class Post extends StatefulWidget {
class _PostState extends State<Post> {
@override
Widget build(BuildContext context) {
List<Widget> c = [];
c.addAll([
DisplayName(account: widget.model.account),
PostBody(
sensitive: widget.model.sensitive,
content: widget.model.content,
spoilerText: widget.model.spoilerText,
media: widget.model.attachments,
forceShow: !widget.hideSensitive,
),
]);
if (!widget.hideActionBar) {
c.add(
PostActionBar(model: widget.model),
);
}
c.add(
RebloggedBy(
account: widget.model.rebloggedBy,
reblogVisible: widget.reblogVisible,
),
);
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DisplayName(account: widget.model.account),
PostBody(
sensitive: widget.model.sensitive,
content: widget.model.content,
spoilerText: widget.model.spoilerText,
media: widget.model.attachments,
forceShow: !widget.hideSensitive,
),
PostActionBar(model: widget.model),
RebloggedBy(
account: widget.model.rebloggedBy,
reblogVisible: widget.reblogVisible,
),
],
children: c,
);
}
}

View File

@ -12,8 +12,16 @@ class PostTextRenderer extends StatelessWidget {
@override
Widget build(BuildContext context) {
MarkdownStyleSheet mdStyle = MarkdownStyleSheet(
a: TextStyle(color: Theme.of(context).colorScheme.secondary));
final MarkdownStyleSheet mdStyle = MarkdownStyleSheet(
a: TextStyle(color: Theme.of(context).colorScheme.secondary),
blockquoteDecoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).colorScheme.secondary,
style: BorderStyle.solid,
)),
);
String s = html2md.convert(input);
return MarkdownBody(
onTapLink: ((text, href, title) {

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'dracula.dart' as color_dracula;
// color schemes to pick from can be added here
@ -7,6 +6,11 @@ import 'dracula.dart' as color_dracula;
final available = [color_dracula.theme];
ThemeData getTheme(CustomColors colors) {
return ThemeData(
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 0,
enableFeedback: false,
hoverElevation: 24,
),
fontFamily: 'Atkinson',
textTheme: TextTheme(
bodyLarge: TextStyle(
@ -133,6 +137,24 @@ ThemeData getTheme(CustomColors colors) {
textSelectionTheme:
TextSelectionThemeData(selectionColor: colors.hintColor),
primaryIconTheme: const IconThemeData(size: 24),
hoverColor: colors.colorScheme.background,
shadowColor: colors.colorScheme.surface,
focusColor: colors.hintColor,
indicatorColor: colors.hintColor,
disabledColor: colors.hintColor,
unselectedWidgetColor: colors.hintColor,
toggleableActiveColor: colors.colorScheme.primary,
splashColor: colors.colorScheme.onSurface,
highlightColor: colors.hintColor,
inputDecorationTheme: InputDecorationTheme(
fillColor: colors.colorScheme.background,
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: colors.hintColor,
width: 2,
),
),
),
);
}