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 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; 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/account/account.dart';
import 'package:loris/business_logic/timeline/media.dart'; import 'package:loris/business_logic/timeline/media.dart';
import '../../global.dart' as global; import '../../global.dart' as global;
@ -25,6 +27,32 @@ extension VisibilityExtension on Visibility {
return true; 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 { class PostModel implements Comparable {

View File

@ -40,6 +40,13 @@
"interacted-with-you": "interacted with you", "interacted-with-you": "interacted with you",
"on-remote-instance": "on remote instance", "on-remote-instance": "on remote instance",
"reblog": "reblog", "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:localization/localization.dart';
import 'package:loris/business_logic/notifications/notifs.dart'; import 'package:loris/business_logic/notifications/notifs.dart';
import 'package:loris/pages/notifications/single_notif.dart'; import 'package:loris/pages/notifications/single_notif.dart';
import 'package:loris/partials/loadingbox.dart';
import '../../business_logic/websocket.dart' as websocket; import '../../business_logic/websocket.dart' as websocket;
final notifStream = StreamController<int>.broadcast(); 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 @override
void initState() { void initState() {
super.initState(); super.initState();
@ -96,7 +112,19 @@ class _NotificationsState extends State<Notifications> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
Container(), Container(
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () {
reload();
},
icon: const Icon(Icons.refresh))
],
),
),
Expanded( Expanded(
child: ListView.separated( child: ListView.separated(
controller: _controller, controller: _controller,

View File

@ -231,9 +231,11 @@ class _InteractionButtonState extends State<InteractionButton> {
} }
}, },
); );
return Column( return SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
children: c, mainAxisAlignment: MainAxisAlignment.center,
children: c,
),
); );
})); }));
} }

View File

@ -14,9 +14,11 @@ class Post extends StatefulWidget {
required this.model, required this.model,
Key? key, Key? key,
this.reblogVisible = true, this.reblogVisible = true,
this.hideSensitive = true,
}) : super(key: key); }) : super(key: key);
final tl.PostModel model; final tl.PostModel model;
final bool reblogVisible; final bool reblogVisible;
final bool hideSensitive;
@override @override
State<Post> createState() => _PostState(); State<Post> createState() => _PostState();
@ -35,6 +37,7 @@ class _PostState extends State<Post> {
content: widget.model.content, content: widget.model.content,
spoilerText: widget.model.spoilerText, spoilerText: widget.model.spoilerText,
media: widget.model.attachments, media: widget.model.attachments,
forceShow: !widget.hideSensitive,
), ),
PostActionBar(model: widget.model), PostActionBar(model: widget.model),
RebloggedBy( RebloggedBy(
@ -163,11 +166,13 @@ class PostBody extends StatefulWidget {
required this.spoilerText, required this.spoilerText,
required this.content, required this.content,
required this.media, required this.media,
required this.forceShow,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final String content; final String content;
final String spoilerText; final String spoilerText;
final bool sensitive; final bool sensitive;
final bool forceShow;
final List<MediaAttachmentModel> media; final List<MediaAttachmentModel> media;
@override @override
@ -178,11 +183,15 @@ class _PostBodyState extends State<PostBody> {
bool visible = false; bool visible = false;
String cwButtonText = "show".i18n(); String cwButtonText = "show".i18n();
Icon cwButtonIcon = const Icon(Icons.visibility); Icon cwButtonIcon = const Icon(Icons.visibility);
@override
void initState() {
visible = !widget.sensitive;
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!widget.sensitive) {
visible = true;
}
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.fromLTRB(0, 6, 0, 6), padding: const EdgeInsets.fromLTRB(0, 6, 0, 6),
@ -190,44 +199,50 @@ class _PostBodyState extends State<PostBody> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Visibility( Visibility(
visible: widget.sensitive, visible: widget.forceShow ? false : widget.sensitive,
child: SelectableText.rich( child: Column(
TextSpan( children: [
text: "", SelectableText.rich(
children: [ TextSpan(
WidgetSpan( children: [
child: OutlinedButton.icon( WidgetSpan(
onPressed: () { child: OutlinedButton.icon(
setState(() { onPressed: () {
visible = !visible; setState(() {
if (visible) { visible = !visible;
cwButtonIcon = const Icon(Icons.visibility_off); if (visible) {
cwButtonText = "hide".i18n(); cwButtonIcon = const Icon(Icons.visibility_off);
} else { cwButtonText = "hide".i18n();
cwButtonText = "show".i18n(); } else {
cwButtonIcon = const Icon(Icons.visibility); cwButtonText = "show".i18n();
} cwButtonIcon = const Icon(Icons.visibility);
}); }
}, });
icon: cwButtonIcon, },
label: Text(cwButtonText), icon: cwButtonIcon,
), label: Text(cwButtonText),
),
),
const WidgetSpan(
child: SizedBox(
width: 8,
)),
TextSpan(text: widget.spoilerText),
],
), ),
const WidgetSpan( ),
child: SizedBox( const SizedBox(
width: 8, height: 8,
)), )
TextSpan(text: widget.spoilerText), ],
],
),
), ),
), ),
Visibility( Visibility(
visible: visible, visible: visible || widget.forceShow,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PostTextRenderer(htmlInput: widget.content), PostTextRenderer(input: widget.content),
MediaAttachments(models: widget.media), 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/business_logic/timeline/timeline.dart';
import 'package:loris/partials/interaction_button.dart'; import 'package:loris/partials/interaction_button.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:clipboard/clipboard.dart';
void popupPostOptions(context, PostModel model) { void popupPostOptions(context, PostModel model) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) => PostOptions(model: model),
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
])));
},
); );
} }
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:flutter/material.dart';
import 'package:html/dom.dart' as dom; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:html/parser.dart' as parser; import 'package:html2md/html2md.dart' as html2md;
import 'package:url_launcher/url_launcher.dart';
class PostTextRenderer extends StatelessWidget { class PostTextRenderer extends StatelessWidget {
const PostTextRenderer({ const PostTextRenderer({
required this.htmlInput, required this.input,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
final String htmlInput; final String input;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
dom.Document document = parser.parse(htmlInput); MarkdownStyleSheet mdStyle = MarkdownStyleSheet(
final List<InlineSpan> children = a: TextStyle(color: Theme.of(context).colorScheme.secondary));
createSpansFromDoc(document.body!.children); String s = html2md.convert(input);
return SelectableText.rich( return MarkdownBody(
TextSpan( onTapLink: ((text, href, title) {
style: Theme.of(context).textTheme.bodyMedium, if (href != null) {
text: "", launchUrl(Uri.parse(href));
children: children, }
), }),
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:flutter/material.dart';
import 'package:localization/localization.dart';
import 'package:loris/partials/post.dart'; import 'package:loris/partials/post.dart';
import '../business_logic/timeline/timeline.dart' as logic; import '../business_logic/timeline/timeline.dart' as logic;
import '../global.dart' as global; import '../global.dart' as global;
class Thread extends StatelessWidget { class Thread extends StatefulWidget {
const Thread({required this.model, Key? key}) : super(key: key); const Thread({required this.model, Key? key}) : super(key: key);
final logic.ThreadModel model; final logic.ThreadModel model;
@override
State<Thread> createState() => _ThreadState();
}
class _ThreadState extends State<Thread> {
bool anySensitivePosts = false;
bool showSensitive = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
List<Post> posts = []; List<Widget> c = [];
for (int i = 0; i < model.posts.length; i++) { for (var element in widget.model.posts) {
posts.add(Post(model: model.posts[i])); 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( return Row(
@ -35,7 +71,7 @@ class Thread extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Column( child: Column(
children: posts, children: c,
), ),
), ),
), ),

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'dracula.dart' as color_dracula; import 'dracula.dart' as color_dracula;
// color schemes to pick from can be added here // color schemes to pick from can be added here

View File

@ -1,6 +1,13 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.1"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -29,6 +36,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
clipboard:
dependency: "direct main"
description:
name: clipboard
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -84,7 +98,7 @@ packages:
name: file name: file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.2" version: "6.1.4"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -102,6 +116,13 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -119,6 +140,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.15.0" version: "0.15.0"
html2md:
dependency: "direct main"
description:
name: html2md
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.6"
http: http:
dependency: "direct main" dependency: "direct main"
description: description:
@ -161,6 +189,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
markdown:
dependency: "direct main"
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -431,7 +466,7 @@ packages:
name: xdg_directories name: xdg_directories
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.2.0+1" version: "0.2.0+2"
sdks: sdks:
dart: ">=2.17.3 <3.0.0" dart: ">=2.17.3 <3.0.0"
flutter: ">=3.0.0" flutter: ">=3.0.0"

View File

@ -44,6 +44,10 @@ dependencies:
shelf: ^1.3.1 shelf: ^1.3.1
html: ^0.15.0 html: ^0.15.0
web_socket_channel: ^2.2.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: dev_dependencies:
flutter_test: flutter_test: