This commit is contained in:
zoe 2022-08-28 20:55:23 +02:00
parent 383fb16298
commit c6fe34e66e
11 changed files with 316 additions and 127 deletions

View File

@ -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 {

View File

@ -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"
}

View File

@ -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,

View File

@ -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,
),
);
}));
}

View File

@ -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),
],
),

View File

@ -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
],
),
);
}
}

View File

@ -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,
),
),
);
}

View File

@ -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,
),
),
),

View File

@ -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

View File

@ -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"

View File

@ -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: