we have posting!
This commit is contained in:
parent
cb81269576
commit
116d7e6c78
|
@ -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))
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue