hmmm
This commit is contained in:
parent
383fb16298
commit
c6fe34e66e
|
@ -1,5 +1,7 @@
|
|||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:localization/localization.dart';
|
||||
import 'package:loris/business_logic/account/account.dart';
|
||||
import 'package:loris/business_logic/timeline/media.dart';
|
||||
import '../../global.dart' as global;
|
||||
|
@ -25,6 +27,32 @@ extension VisibilityExtension on Visibility {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String get name {
|
||||
switch (this) {
|
||||
case Visibility.direct:
|
||||
return "direct-visibility".i18n();
|
||||
case Visibility.private:
|
||||
return "private-visibility".i18n();
|
||||
case Visibility.public:
|
||||
return "public-visibility".i18n();
|
||||
case Visibility.unlisted:
|
||||
return "unlisted-visibility".i18n();
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case Visibility.direct:
|
||||
return Icons.message;
|
||||
case Visibility.private:
|
||||
return Icons.lock;
|
||||
case Visibility.public:
|
||||
return Icons.public;
|
||||
case Visibility.unlisted:
|
||||
return Icons.lock_open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PostModel implements Comparable {
|
||||
|
|
|
@ -40,6 +40,13 @@
|
|||
"interacted-with-you": "interacted with you",
|
||||
"on-remote-instance": "on remote instance",
|
||||
"reblog": "reblog",
|
||||
"like": "like"
|
||||
"like": "like",
|
||||
"load-older-notifications": "load older notifications",
|
||||
"copied-post-by": "copied link to post by:",
|
||||
"copy-url-to-clipboard": "copy url to clipboard",
|
||||
"unlisted-visibility": "unlisted",
|
||||
"public-visibility": "public",
|
||||
"private-visibility": "private",
|
||||
"direct-visibility": "dm"
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:localization/localization.dart';
|
||||
import 'package:loris/business_logic/notifications/notifs.dart';
|
||||
import 'package:loris/pages/notifications/single_notif.dart';
|
||||
import 'package:loris/partials/loadingbox.dart';
|
||||
import '../../business_logic/websocket.dart' as websocket;
|
||||
|
||||
final notifStream = StreamController<int>.broadcast();
|
||||
|
@ -41,6 +42,21 @@ class _NotificationsState extends State<Notifications> {
|
|||
}
|
||||
}
|
||||
|
||||
void reload() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_controller.animateTo(
|
||||
0,
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
maxIdData = {};
|
||||
notifs = [const LoadingBox()];
|
||||
});
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -96,7 +112,19 @@ class _NotificationsState extends State<Notifications> {
|
|||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
reload();
|
||||
},
|
||||
icon: const Icon(Icons.refresh))
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
controller: _controller,
|
||||
|
|
|
@ -231,9 +231,11 @@ class _InteractionButtonState extends State<InteractionButton> {
|
|||
}
|
||||
},
|
||||
);
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: c,
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: c,
|
||||
),
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -14,9 +14,11 @@ class Post extends StatefulWidget {
|
|||
required this.model,
|
||||
Key? key,
|
||||
this.reblogVisible = true,
|
||||
this.hideSensitive = true,
|
||||
}) : super(key: key);
|
||||
final tl.PostModel model;
|
||||
final bool reblogVisible;
|
||||
final bool hideSensitive;
|
||||
|
||||
@override
|
||||
State<Post> createState() => _PostState();
|
||||
|
@ -35,6 +37,7 @@ class _PostState extends State<Post> {
|
|||
content: widget.model.content,
|
||||
spoilerText: widget.model.spoilerText,
|
||||
media: widget.model.attachments,
|
||||
forceShow: !widget.hideSensitive,
|
||||
),
|
||||
PostActionBar(model: widget.model),
|
||||
RebloggedBy(
|
||||
|
@ -163,11 +166,13 @@ class PostBody extends StatefulWidget {
|
|||
required this.spoilerText,
|
||||
required this.content,
|
||||
required this.media,
|
||||
required this.forceShow,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final String content;
|
||||
final String spoilerText;
|
||||
final bool sensitive;
|
||||
final bool forceShow;
|
||||
final List<MediaAttachmentModel> media;
|
||||
|
||||
@override
|
||||
|
@ -178,11 +183,15 @@ class _PostBodyState extends State<PostBody> {
|
|||
bool visible = false;
|
||||
String cwButtonText = "show".i18n();
|
||||
Icon cwButtonIcon = const Icon(Icons.visibility);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
visible = !widget.sensitive;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.sensitive) {
|
||||
visible = true;
|
||||
}
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(0, 6, 0, 6),
|
||||
|
@ -190,44 +199,50 @@ class _PostBodyState extends State<PostBody> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: widget.sensitive,
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
text: "",
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
visible = !visible;
|
||||
if (visible) {
|
||||
cwButtonIcon = const Icon(Icons.visibility_off);
|
||||
cwButtonText = "hide".i18n();
|
||||
} else {
|
||||
cwButtonText = "show".i18n();
|
||||
cwButtonIcon = const Icon(Icons.visibility);
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: cwButtonIcon,
|
||||
label: Text(cwButtonText),
|
||||
),
|
||||
visible: widget.forceShow ? false : widget.sensitive,
|
||||
child: Column(
|
||||
children: [
|
||||
SelectableText.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
visible = !visible;
|
||||
if (visible) {
|
||||
cwButtonIcon = const Icon(Icons.visibility_off);
|
||||
cwButtonText = "hide".i18n();
|
||||
} else {
|
||||
cwButtonText = "show".i18n();
|
||||
cwButtonIcon = const Icon(Icons.visibility);
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: cwButtonIcon,
|
||||
label: Text(cwButtonText),
|
||||
),
|
||||
),
|
||||
const WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
)),
|
||||
TextSpan(text: widget.spoilerText),
|
||||
],
|
||||
),
|
||||
const WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 8,
|
||||
)),
|
||||
TextSpan(text: widget.spoilerText),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: visible,
|
||||
visible: visible || widget.forceShow,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
PostTextRenderer(htmlInput: widget.content),
|
||||
PostTextRenderer(input: widget.content),
|
||||
MediaAttachments(models: widget.media),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -4,43 +4,106 @@ import 'package:loris/business_logic/interactions/interactions.dart';
|
|||
import 'package:loris/business_logic/timeline/timeline.dart';
|
||||
import 'package:loris/partials/interaction_button.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
|
||||
void popupPostOptions(context, PostModel model) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
List<Widget?> c = [
|
||||
SelectableText("post-options".i18n(),
|
||||
style: Theme.of(context).textTheme.displayMedium),
|
||||
SelectableText(model.createdAt),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(model.uri),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text("show-in-browser".i18n()),
|
||||
),
|
||||
model.visibility.boostable
|
||||
? InteractionButton(
|
||||
model: model,
|
||||
type: InteractionType.reblog,
|
||||
extended: true,
|
||||
)
|
||||
: null,
|
||||
InteractionButton(
|
||||
model: model,
|
||||
type: InteractionType.favorite,
|
||||
extended: true,
|
||||
),
|
||||
];
|
||||
return Scrollable(
|
||||
viewportBuilder: ((context, position) =>
|
||||
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
for (var i in c)
|
||||
if (i != null) i
|
||||
])));
|
||||
},
|
||||
builder: (context) => PostOptions(model: model),
|
||||
);
|
||||
}
|
||||
|
||||
class PostOptions extends StatefulWidget {
|
||||
const PostOptions({Key? key, required this.model}) : super(key: key);
|
||||
final PostModel model;
|
||||
|
||||
@override
|
||||
State<PostOptions> createState() => _PostOptionsState();
|
||||
}
|
||||
|
||||
class _PostOptionsState extends State<PostOptions> {
|
||||
bool justCopied = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget?> c = [
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
SelectableText("post-options".i18n(),
|
||||
style: Theme.of(context).textTheme.displayMedium),
|
||||
SelectableText(
|
||||
widget.model.createdAt
|
||||
.replaceAll("T", " ")
|
||||
.replaceAll("-", ".")
|
||||
.substring(0, 19),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(widget.model.visibility.icon),
|
||||
SelectableText(widget.model.visibility.name),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
FlutterClipboard.copy(widget.model.uri);
|
||||
setState(() {
|
||||
justCopied = true;
|
||||
});
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
justCopied = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.copy),
|
||||
label: Text(
|
||||
justCopied
|
||||
? "${"copied-post-by".i18n()} ${widget.model.account.acct}"
|
||||
: "copy-url-to-clipboard".i18n(),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
launchUrl(
|
||||
Uri.parse(widget.model.uri),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.open_in_browser),
|
||||
label: Text(
|
||||
"show-in-browser".i18n(),
|
||||
),
|
||||
),
|
||||
widget.model.visibility.boostable
|
||||
? InteractionButton(
|
||||
model: widget.model,
|
||||
type: InteractionType.reblog,
|
||||
extended: true,
|
||||
)
|
||||
: null,
|
||||
InteractionButton(
|
||||
model: widget.model,
|
||||
type: InteractionType.favorite,
|
||||
extended: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
];
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
for (var i in c)
|
||||
if (i != null) i
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +1,29 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart' as parser;
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class PostTextRenderer extends StatelessWidget {
|
||||
const PostTextRenderer({
|
||||
required this.htmlInput,
|
||||
required this.input,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final String htmlInput;
|
||||
final String input;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
dom.Document document = parser.parse(htmlInput);
|
||||
final List<InlineSpan> children =
|
||||
createSpansFromDoc(document.body!.children);
|
||||
return SelectableText.rich(
|
||||
TextSpan(
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
text: "",
|
||||
children: children,
|
||||
),
|
||||
MarkdownStyleSheet mdStyle = MarkdownStyleSheet(
|
||||
a: TextStyle(color: Theme.of(context).colorScheme.secondary));
|
||||
String s = html2md.convert(input);
|
||||
return MarkdownBody(
|
||||
onTapLink: ((text, href, title) {
|
||||
if (href != null) {
|
||||
launchUrl(Uri.parse(href));
|
||||
}
|
||||
}),
|
||||
styleSheet: mdStyle,
|
||||
data: s,
|
||||
selectable: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<InlineSpan> createSpansFromDoc(List<dom.Element> elements) {
|
||||
List<InlineSpan> result = [];
|
||||
for (int i = 0; i < elements.length; i += 1) {
|
||||
final e = elements[i];
|
||||
result.add(
|
||||
getSpanForElement(
|
||||
e,
|
||||
elements.length,
|
||||
i,
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
InlineSpan getSpanForElement(dom.Element e, int bodyLength, int pos) {
|
||||
if (e.toString() == "<html p>") {
|
||||
return handleParagraph(e, bodyLength, pos);
|
||||
}
|
||||
return TextSpan(text: e.text);
|
||||
}
|
||||
|
||||
InlineSpan handleParagraph(dom.Element e, int bodyLength, int pos) {
|
||||
String text = e.text;
|
||||
return WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: SelectableText(
|
||||
text,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,53 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:localization/localization.dart';
|
||||
import 'package:loris/partials/post.dart';
|
||||
import '../business_logic/timeline/timeline.dart' as logic;
|
||||
import '../global.dart' as global;
|
||||
|
||||
class Thread extends StatelessWidget {
|
||||
class Thread extends StatefulWidget {
|
||||
const Thread({required this.model, Key? key}) : super(key: key);
|
||||
final logic.ThreadModel model;
|
||||
|
||||
@override
|
||||
State<Thread> createState() => _ThreadState();
|
||||
}
|
||||
|
||||
class _ThreadState extends State<Thread> {
|
||||
bool anySensitivePosts = false;
|
||||
bool showSensitive = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Post> posts = [];
|
||||
for (int i = 0; i < model.posts.length; i++) {
|
||||
posts.add(Post(model: model.posts[i]));
|
||||
List<Widget> c = [];
|
||||
for (var element in widget.model.posts) {
|
||||
c.add(Post(
|
||||
model: element,
|
||||
hideSensitive: !showSensitive,
|
||||
));
|
||||
if (element.sensitive) {
|
||||
anySensitivePosts = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (anySensitivePosts && c.length > 1) {
|
||||
c.insert(
|
||||
0,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showSensitive = !showSensitive;
|
||||
});
|
||||
},
|
||||
icon:
|
||||
Icon(showSensitive ? Icons.visibility_off : Icons.visibility),
|
||||
label: Text(showSensitive ? "hide".i18n() : "show".i18n()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
|
@ -35,7 +71,7 @@ class Thread extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
children: posts,
|
||||
children: c,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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
|
||||
|
|
39
pubspec.lock
39
pubspec.lock
|
@ -1,6 +1,13 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -29,6 +36,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
clipboard:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: clipboard
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -84,7 +98,7 @@ packages:
|
|||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.4"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -102,6 +116,13 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.10+5"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -119,6 +140,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.15.0"
|
||||
html2md:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: html2md
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.6"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -161,6 +189,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -431,7 +466,7 @@ packages:
|
|||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0+1"
|
||||
version: "0.2.0+2"
|
||||
sdks:
|
||||
dart: ">=2.17.3 <3.0.0"
|
||||
flutter: ">=3.0.0"
|
||||
|
|
|
@ -44,6 +44,10 @@ dependencies:
|
|||
shelf: ^1.3.1
|
||||
html: ^0.15.0
|
||||
web_socket_channel: ^2.2.0
|
||||
clipboard: ^0.1.3
|
||||
flutter_markdown: ^0.6.10+5
|
||||
markdown: ^5.0.0
|
||||
html2md: ^1.2.6
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue