diff --git a/lib/create_thread_screen.dart b/lib/create_thread_screen.dart new file mode 100644 index 0000000..29b3ca0 --- /dev/null +++ b/lib/create_thread_screen.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wind/thread_list_model.dart'; +import 'package:wind/thread_model.dart'; + +class CreateThreadScreen extends StatefulWidget { + @override + State createState() => _CreateThreadScreenState(); +} + +class _CreateThreadScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text("Создать новый тред")), + body: Center( + child: Container( + width: 640, + child: Consumer( + builder: (context, value, child) => CreateThreadForm(model: value), + ), + ))); + } +} + +class CreateThreadForm extends StatefulWidget { + CreateThreadForm({Key? key, required this.model}) : super(key: key); + + ThreadModel model; + + @override + CreateThreadFormState createState() { + return CreateThreadFormState(model); + } +} + +// Define a corresponding State class. +// This class holds data related to the form. +class CreateThreadFormState extends State { + CreateThreadFormState(this.model); + + ThreadModel model; + final _formKey = GlobalKey(); + + String _subject = ""; + String _text = ""; + + @override + Widget build(BuildContext context) { + return Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: TextFormField( + onSaved: ((newValue) { + _subject = newValue!; + }), + decoration: InputDecoration( + border: OutlineInputBorder(), labelText: "Название поста"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Пожалуйста, введите название'; + } + return null; + }), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: TextFormField( + onSaved: (newValue) { + _text = newValue!; + }, + minLines: 5, + keyboardType: TextInputType.multiline, + maxLines: 35, + decoration: InputDecoration( + border: OutlineInputBorder(), labelText: "Текст поста"), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Пожалуйста, введите текст'; + } + return null; + }, + )), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + model.createThread(_subject, _text).then((value) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Тред создан!')), + ); + Provider.of(context, listen: false) + .update(); + Navigator.pop(context); + }, onError: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'При создании треда произошла ошибка: ${error.toString()}')), + ); + }); + } + }, + child: Text("Создать")) + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 91274cb..0f9d49b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wind/create_thread_screen.dart'; import 'package:wind/newsgroup_list_view.dart'; import 'package:wind/nntp_client.dart'; import 'package:wind/thread_list_view.dart'; @@ -47,6 +48,9 @@ class MyApp extends StatelessWidget { threadNumber: int.parse(uriData.queryParametersAll['num']!.first)); break; + case '/thread/create': + pageView = CreateThreadScreen(); + break; default: pageView = MyHomePage(title: 'Wind'); break; @@ -82,64 +86,69 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - actions: [ - TextButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Обновление списка тредов...')), - ); - Provider.of(context, listen: false).update(); - }, - label: const Text("Обновить"), - style: TextButton.styleFrom( - primary: Theme.of(context).colorScheme.onPrimary), - icon: Icon(Icons.sync)) - ], - ), - body: Center( - child: Container( - child: Row( - children: [ - SizedBox( - width: 300, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: EdgeInsets.all(16), - child: Text( - "Новостные группы", - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 20), - )), - Expanded( - child: Builder( - builder: (context) => - NewsgroupListView(client: nntpClient))), - ], - ), - ), - const VerticalDivider(thickness: 1, width: 1), - Expanded( - child: Center( - child: Consumer( - builder: ((context, value, child) => ThreadListView())), - )) - ], - ), + return Consumer( + builder: (context, value, child) => Scaffold( + appBar: AppBar( + title: Text(widget.title), + actions: value.currentGroup != "" + ? [ + TextButton.icon( + onPressed: () => + Navigator.pushNamed(context, "/thread/create"), + icon: Icon(Icons.add), + label: Text("Создать тред"), + style: TextButton.styleFrom( + primary: Theme.of(context).colorScheme.onPrimary), + ), + TextButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Обновление списка тредов...')), + ); + Provider.of(context, listen: false) + .update(); + }, + label: const Text("Обновить"), + style: TextButton.styleFrom( + primary: Theme.of(context).colorScheme.onPrimary), + icon: Icon(Icons.sync)) + ] + : [], ), - )); + body: Center( + child: Container( + child: Row( + children: [ + SizedBox( + width: 300, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.all(16), + child: Text( + "Новостные группы", + style: TextStyle( + fontWeight: FontWeight.bold, fontSize: 20), + )), + Expanded( + child: Builder( + builder: (context) => + NewsgroupListView(client: nntpClient))), + ], + ), + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: Center( + child: Consumer( + builder: ((context, value, child) => ThreadListView())), + )) + ], + ), + ), + )), + ); } } diff --git a/lib/message_item_view.dart b/lib/message_item_view.dart index e64116a..9364698 100644 --- a/lib/message_item_view.dart +++ b/lib/message_item_view.dart @@ -18,7 +18,7 @@ class MessageItemView extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: this.isOpPost + padding: this.isOpPost ? EdgeInsets.only(bottom: 10, left: 10, right: 10, top: 16) : isLast ? EdgeInsets.only(left: 10, right: 10, bottom: 16) diff --git a/lib/thread_list_model.dart b/lib/thread_list_model.dart index 4dd2350..4560616 100644 --- a/lib/thread_list_model.dart +++ b/lib/thread_list_model.dart @@ -40,7 +40,7 @@ class ThreadListModel extends ChangeNotifier { _curItems.add(ThreadItem( msg.getHeaderValue("Message-Id")!, number, - msg.getHeaderValue("Subject")!, + msg.decodeSubject()!, msg.getHeaderValue("From")!, msg.getHeaderValue("Date")!, msg.decodeTextPlainPart()!)); diff --git a/lib/thread_model.dart b/lib/thread_model.dart index 5c5d3f3..3e28517 100644 --- a/lib/thread_model.dart +++ b/lib/thread_model.dart @@ -13,7 +13,7 @@ class ThreadModel extends ChangeNotifier { var mi = MessageItem( msg.getHeaderValue("Message-Id")!, number, - msg.getHeaderValue("Subject")!, + msg.decodeSubject()!, msg.getHeaderValue("From")!, msg.getHeaderValue("Date")!, msg.decodeTextPlainPart()!); @@ -37,7 +37,7 @@ class ThreadModel extends ChangeNotifier { null, message.getHeaderValue("From")!, message.getHeaderValue("Date")!, - message.decodeTextPlainPart()!)); + message.decodeTextPlainPart()!.trim())); }); return items; @@ -45,7 +45,7 @@ class ThreadModel extends ChangeNotifier { Future postMessage(MimeMessage opPost, String text) async { var msg = MessageBuilder.buildSimpleTextMessage( - MailAddress.empty(), [], text, + MailAddress.empty(), [], text.trim(), subject: "Re: " + opPost.decodeSubject()!); msg.setHeader("From", "anonymous"); msg.addHeader("In-Reply-To", opPost.getHeaderValue("Message-Id")); @@ -54,6 +54,15 @@ class ThreadModel extends ChangeNotifier { return await client!.postArticle(msg); } + Future createThread(String subject, String text) async { + var msg = MessageBuilder.buildSimpleTextMessage( + MailAddress.empty(), [], text.trim(), + subject: subject); + msg.setHeader("From", "anonymous"); + msg.addHeader("Newsgroups", client!.currentGroup!); + return await client!.postArticle(msg); + } + void update() { notifyListeners(); }