From a3b4706ca2340c2d4f2297962f5042ccd8eb4135 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 23 Mar 2024 23:05:04 +0800 Subject: [PATCH] :sparkles: Explore page --- lib/main.dart | 8 +- lib/models/author.dart | 59 +++++++++++++ lib/models/feed.dart | 154 ++++++++++++++++++++++++++++++++ lib/models/pagination.dart | 17 ++++ lib/router.dart | 4 +- lib/screens/account.dart | 124 +++++++++++++------------- lib/screens/application.dart | 36 -------- lib/screens/dashboard.dart | 93 -------------------- lib/screens/explore.dart | 78 +++++++++++++++++ lib/screens/notifications.dart | 156 ++++++++++++++++----------------- lib/widgets/feed.dart | 43 +++++++++ lib/widgets/name_card.dart | 6 +- lib/widgets/navigation.dart | 2 +- pubspec.lock | 42 ++++++++- pubspec.yaml | 2 + 15 files changed, 540 insertions(+), 284 deletions(-) create mode 100644 lib/models/author.dart create mode 100644 lib/models/feed.dart create mode 100644 lib/models/pagination.dart delete mode 100644 lib/screens/application.dart delete mode 100644 lib/screens/dashboard.dart create mode 100644 lib/screens/explore.dart create mode 100644 lib/widgets/feed.dart diff --git a/lib/main.dart b/lib/main.dart index 9bce2f9..c002b67 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,9 +43,11 @@ class SolarAgent extends StatelessWidget { initialEntries: [ OverlayEntry( builder: (context) => SafeArea( - child: Scaffold( - body: child, - bottomNavigationBar: const AgentNavigation(), + child: SafeArea( + child: Scaffold( + body: child, + bottomNavigationBar: const AgentNavigation(), + ), ), ), ) diff --git a/lib/models/author.dart b/lib/models/author.dart new file mode 100644 index 0000000..4301757 --- /dev/null +++ b/lib/models/author.dart @@ -0,0 +1,59 @@ +class Author { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String name; + String nick; + String avatar; + String banner; + String description; + String emailAddress; + int powerLevel; + int externalId; + + Author({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.name, + required this.nick, + required this.avatar, + required this.banner, + required this.description, + required this.emailAddress, + required this.powerLevel, + required this.externalId, + }); + + factory Author.fromJson(Map json) => Author( + id: json["id"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + deletedAt: json["deleted_at"], + name: json["name"], + nick: json["nick"], + avatar: json["avatar"], + banner: json["banner"], + description: json["description"], + emailAddress: json["email_address"], + powerLevel: json["power_level"], + externalId: json["external_id"], + ); + + Map toJson() => { + "id": id, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "deleted_at": deletedAt, + "name": name, + "nick": nick, + "avatar": avatar, + "banner": banner, + "description": description, + "email_address": emailAddress, + "power_level": powerLevel, + "external_id": externalId, + }; +} diff --git a/lib/models/feed.dart b/lib/models/feed.dart new file mode 100644 index 0000000..06269e8 --- /dev/null +++ b/lib/models/feed.dart @@ -0,0 +1,154 @@ +import 'package:solaragent/models/author.dart'; + +class Feed { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String alias; + String title; + String description; + String content; + String modelType; + int commentCount; + int reactionCount; + int authorId; + int? realmId; + Author author; + List? attachments; + Map? reactionList; + + Feed({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.alias, + required this.title, + required this.description, + required this.content, + required this.modelType, + required this.commentCount, + required this.reactionCount, + required this.authorId, + this.realmId, + required this.author, + this.attachments, + this.reactionList, + }); + + factory Feed.fromJson(Map json) => Feed( + id: json["id"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + deletedAt: json["deleted_at"], + alias: json["alias"], + title: json["title"], + description: json["description"], + content: json["content"], + modelType: json["model_type"], + commentCount: json["comment_count"], + reactionCount: json["reaction_count"], + authorId: json["author_id"], + realmId: json["realm_id"], + author: Author.fromJson(json["author"]), + attachments: json["attachments"] != null + ? List.from( + json["attachments"].map((x) => Attachment.fromJson(x))) + : List.empty(), + reactionList: json["reaction_list"], + ); + + Map toJson() => { + "id": id, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "deleted_at": deletedAt, + "alias": alias, + "title": title, + "description": description, + "content": content, + "model_type": modelType, + "comment_count": commentCount, + "reaction_count": reactionCount, + "author_id": authorId, + "realm_id": realmId, + "author": author.toJson(), + "attachments": attachments == null + ? List.empty() + : List.from(attachments!.map((x) => x.toJson())), + "reaction_list": reactionList, + }; +} + +class Attachment { + int id; + DateTime createdAt; + DateTime updatedAt; + DateTime? deletedAt; + String fileId; + int filesize; + String filename; + String mimetype; + int type; + String externalUrl; + Author author; + int? articleId; + int? momentId; + int? commentId; + int? authorId; + + Attachment({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.fileId, + required this.filesize, + required this.filename, + required this.mimetype, + required this.type, + required this.externalUrl, + required this.author, + this.articleId, + this.momentId, + this.commentId, + this.authorId, + }); + + factory Attachment.fromJson(Map json) => Attachment( + id: json["id"], + createdAt: DateTime.parse(json["created_at"]), + updatedAt: DateTime.parse(json["updated_at"]), + deletedAt: json["deleted_at"], + fileId: json["file_id"], + filesize: json["filesize"], + filename: json["filename"], + mimetype: json["mimetype"], + type: json["type"], + externalUrl: json["external_url"], + author: Author.fromJson(json["author"]), + articleId: json["article_id"], + momentId: json["moment_id"], + commentId: json["comment_id"], + authorId: json["author_id"], + ); + + Map toJson() => { + "id": id, + "created_at": createdAt.toIso8601String(), + "updated_at": updatedAt.toIso8601String(), + "deleted_at": deletedAt, + "file_id": fileId, + "filesize": filesize, + "filename": filename, + "mimetype": mimetype, + "type": type, + "external_url": externalUrl, + "author": author.toJson(), + "article_id": articleId, + "moment_id": momentId, + "comment_id": commentId, + "author_id": authorId, + }; +} diff --git a/lib/models/pagination.dart b/lib/models/pagination.dart new file mode 100644 index 0000000..8330eb3 --- /dev/null +++ b/lib/models/pagination.dart @@ -0,0 +1,17 @@ +class PaginationResult { + int count; + List? data; + + PaginationResult({ + required this.count, + this.data, + }); + + factory PaginationResult.fromJson(Map json) => + PaginationResult(count: json["count"], data: json["data"]); + + Map toJson() => { + "count": count, + "data": data, + }; +} diff --git a/lib/router.dart b/lib/router.dart index 736ab5f..c0c6880 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,13 +1,13 @@ import 'package:go_router/go_router.dart'; import 'package:solaragent/screens/account.dart'; -import 'package:solaragent/screens/dashboard.dart'; +import 'package:solaragent/screens/explore.dart'; import 'package:solaragent/screens/notifications.dart'; final router = GoRouter( routes: [ GoRoute( path: '/', - builder: (context, state) => const DashboardScreen(), + builder: (context, state) => const ExploreScreen(), ), GoRoute( path: '/notifications', diff --git a/lib/screens/account.dart b/lib/screens/account.dart index ab530a9..4b36eb2 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; import 'package:solaragent/auth.dart'; import 'package:solaragent/screens/about.dart'; import 'package:solaragent/widgets/name_card.dart'; @@ -27,72 +26,69 @@ class _AccountScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), - child: Column(children: [ - Padding( - padding: const EdgeInsets.only(top: 20), - child: NameCard( - onLogin: () async { - await authClient.signin(context); - var authorized = await authClient.isAuthorized(); - setState(() { - isAuthorized = authorized; - }); - }, - ), + body: Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), + child: Column(children: [ + Padding( + padding: const EdgeInsets.only(top: 20), + child: NameCard( + onLogin: () async { + await authClient.signin(context); + var authorized = await authClient.isAuthorized(); + setState(() { + isAuthorized = authorized; + }); + }, ), - Padding( - padding: const EdgeInsets.only(top: 5), - child: Wrap( - spacing: 5, - children: [ - FutureBuilder( - future: authClient.isAuthorized(), - builder: - (BuildContext context, AsyncSnapshot snapshot) { - return (snapshot.hasData && snapshot.data == true) - ? InkWell( - borderRadius: - const BorderRadius.all(Radius.circular(40)), - splashColor: Colors.indigo.withAlpha(30), - onTap: () async { - authClient.signoff(); - var authorized = - await authClient.isAuthorized(); - setState(() { - isAuthorized = authorized; - }); - }, - child: const ListTile( - leading: Icon(Icons.logout), - title: Text('Logout'), - ), - ) - : Container(); - }, + ), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Wrap( + spacing: 5, + children: [ + FutureBuilder( + future: authClient.isAuthorized(), + builder: + (BuildContext context, AsyncSnapshot snapshot) { + return (snapshot.hasData && snapshot.data == true) + ? InkWell( + borderRadius: + const BorderRadius.all(Radius.circular(40)), + splashColor: Colors.indigo.withAlpha(30), + onTap: () async { + authClient.signoff(); + var authorized = await authClient.isAuthorized(); + setState(() { + isAuthorized = authorized; + }); + }, + child: const ListTile( + leading: Icon(Icons.logout), + title: Text('Logout'), + ), + ) + : Container(); + }, + ), + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(40)), + splashColor: Colors.indigo.withAlpha(30), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AboutScreen(), + )); + }, + child: const ListTile( + leading: Icon(Icons.info_outline), + title: Text('About'), ), - InkWell( - borderRadius: const BorderRadius.all(Radius.circular(40)), - splashColor: Colors.indigo.withAlpha(30), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AboutScreen(), - )); - }, - child: const ListTile( - leading: Icon(Icons.info_outline), - title: Text('About'), - ), - ), - ], - ), + ), + ], ), - ]), - ), + ), + ]), ), ); } diff --git a/lib/screens/application.dart b/lib/screens/application.dart deleted file mode 100644 index 7e67182..0000000 --- a/lib/screens/application.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -class ApplicationScreen extends StatelessWidget { - final Uri link; - final String title; - - const ApplicationScreen({super.key, required this.link, required this.title}); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - bottom: false, - child: Stack( - children: [ - WebViewWidget( - controller: WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Colors.white) - ..setNavigationDelegate(NavigationDelegate( - onPageStarted: (_) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text("Swipe from left to back to dashboard."), - )); - } - )) - ..loadRequest(link) - ..clearCache(), - ), - ], - ), - ), - ); - } -} diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart deleted file mode 100644 index ec88a89..0000000 --- a/lib/screens/dashboard.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; -import 'package:solaragent/screens/application.dart'; -import 'package:http/http.dart' as http; - -class DashboardScreen extends StatefulWidget { - const DashboardScreen({super.key}); - - @override - State createState() => _DashboardScreenState(); -} - -class _DashboardScreenState extends State { - final client = http.Client(); - final directoryEndpoint = - Uri.parse('https://id.solsynth.dev/.well-known'); - - List directory = List.empty(); - - @override - void initState() { - super.initState(); - _pullDirectory(); - } - - Future _pullDirectory() async { - var response = await client.get(directoryEndpoint); - if (response.statusCode == 200) { - setState(() { - directory = jsonDecode(utf8.decode(response.bodyBytes))["directory"]; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: Padding( - padding: const EdgeInsets.only(left: 20, right: 20, top: 30), - child: GridView.count( - crossAxisCount: 2, - children: directory.map((element) { - return Card( - child: InkWell( - onTap: () async { - var link = element["link"]; - Navigator.of(context, rootNavigator: true) - .push(MaterialPageRoute( - builder: (context) => ApplicationScreen( - link: Uri.parse(link), - title: element["name"], - ), - )); - }, - splashColor: Colors.indigo.withAlpha(30), - child: Padding( - padding: const EdgeInsets.all(10), - child: Wrap( - spacing: 8.0, - children: [ - Padding( - padding: const EdgeInsets.only(left: 6, top: 2), - child: Card( - shape: const CircleBorder(), - elevation: 0, - color: Colors.indigo.withAlpha(70), - child: const Padding( - padding: EdgeInsets.all(16), - child: Icon(Icons.apps), - ), - ), - ), - ListTile( - title: Text(element['name']), - subtitle: Padding( - padding: const EdgeInsets.only(top: 2), - child: Text(element['description'], style: Theme.of(context).textTheme.bodySmall), - ), - ), - ], - ), - ), - ), - ); - }).toList(), - ), - ), - ), - ); - } -} diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart new file mode 100644 index 0000000..c1282d6 --- /dev/null +++ b/lib/screens/explore.dart @@ -0,0 +1,78 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:solaragent/models/feed.dart'; +import 'package:solaragent/models/pagination.dart'; +import 'package:http/http.dart' as http; +import 'package:solaragent/widgets/feed.dart'; + +class ExploreScreen extends StatefulWidget { + const ExploreScreen({super.key}); + + @override + State createState() => _ExploreScreenState(); +} + +class _ExploreScreenState extends State { + static const pageSize = 5; + + final client = http.Client(); + + final PagingController paginationController = + PagingController(firstPageKey: 0); + + List feed = List.empty(); + + @override + void initState() { + super.initState(); + paginationController.addPageRequestListener((pageKey) { + pullFeed(pageKey); + }); + } + + Future pullFeed(int pageKey) async { + var offset = pageKey; + var take = pageSize; + + var uri = + Uri.parse('https://co.solsynth.dev/api/feed?take=$take&offset=$offset'); + + var res = await client.get(uri); + if (res.statusCode == 200) { + final result = + PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); + final isLastPage = (result.data?.length ?? 0) < pageSize; + if (isLastPage) { + paginationController.appendLastPage(feed); + } else { + final feed = result.data!.map((x) => Feed.fromJson(x)).toList(); + final nextPageKey = pageKey + feed.length; + paginationController.appendPage(feed, nextPageKey); + } + } else { + paginationController.error = utf8.decode(res.bodyBytes); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: PagedListView( + pagingController: paginationController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) => FeedItem( + item: item, + ), + ), + ), + ); + } + + @override + void dispose() { + paginationController.dispose(); + super.dispose(); + } +} diff --git a/lib/screens/notifications.dart b/lib/screens/notifications.dart index 132d97a..5742b00 100644 --- a/lib/screens/notifications.dart +++ b/lib/screens/notifications.dart @@ -46,94 +46,92 @@ class _NotificationScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea( - child: RefreshIndicator( - onRefresh: pullNotifications, - child: CustomScrollView( - slivers: [ - // Title - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 20), - child: ListTile( - title: Text( - 'Notifications', - style: Theme.of(context).textTheme.headlineSmall, - ), + body: RefreshIndicator( + onRefresh: pullNotifications, + child: CustomScrollView( + slivers: [ + // Title + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 20), + child: ListTile( + title: Text( + 'Notifications', + style: Theme.of(context).textTheme.headlineSmall, ), ), ), - // Content - notifications.isEmpty - ? SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 10), - color: Colors.grey[300], - child: const ListTile( - leading: Icon(Icons.check), - title: Text('You\'re done!'), - subtitle: Text( - 'There are no notifications unread for you.', - ), + ), + // Content + notifications.isEmpty + ? SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + color: Colors.grey[300], + child: const ListTile( + leading: Icon(Icons.check), + title: Text('You\'re done!'), + subtitle: Text( + 'There are no notifications unread for you.', ), ), - ) - : SliverList.builder( - itemCount: notifications.length, - itemBuilder: (BuildContext context, int index) { - var element = notifications[index]; - return Dismissible( - key: Key('notification-$index'), - onDismissed: (direction) { - var subject = element["subject"]; - markAsRead(element).then((value) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: RichText( - text: TextSpan(children: [ - TextSpan( - text: subject, - style: const TextStyle( - fontWeight: FontWeight.bold), - ), - const TextSpan( - text: " is marked as read", - ) - ]), - ), - ), - ); - }); - setState(() { - notifications.removeAt(index); - }); - }, - background: Container( - color: Colors.green, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ListTile( - title: Text(element["subject"]), - subtitle: Text(element["content"]), - ), - ), - ); - }, ), - // Tips - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 10), - child: Text( - "Pull to refresh, swipe to dismiss", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodySmall, + ) + : SliverList.builder( + itemCount: notifications.length, + itemBuilder: (BuildContext context, int index) { + var element = notifications[index]; + return Dismissible( + key: Key('notification-$index'), + onDismissed: (direction) { + var subject = element["subject"]; + markAsRead(element).then((value) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: RichText( + text: TextSpan(children: [ + TextSpan( + text: subject, + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + const TextSpan( + text: " is marked as read", + ) + ]), + ), + ), + ); + }); + setState(() { + notifications.removeAt(index); + }); + }, + background: Container( + color: Colors.green, + ), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ListTile( + title: Text(element["subject"]), + subtitle: Text(element["content"]), + ), + ), + ); + }, ), + // Tips + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.only(top: 12), + child: Text( + "Pull to refresh, swipe to dismiss", + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, ), ), - ], - ), + ), + ], ), ), ); diff --git a/lib/widgets/feed.dart b/lib/widgets/feed.dart new file mode 100644 index 0000000..fc524f2 --- /dev/null +++ b/lib/widgets/feed.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:solaragent/models/feed.dart'; + +class FeedItem extends StatelessWidget { + final Feed item; + + const FeedItem({super.key, required this.item}); + + String getDescription(String desc) => + desc.isEmpty ? "No description yet." : desc; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + Container( + color: Colors.grey[50], + child: ListTile( + title: Text(item.author.name), + leading: CircleAvatar( + backgroundImage: NetworkImage(item.author.avatar), + ), + subtitle: Text( + getDescription(item.author.description), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + ), + ), + ), + Markdown( + data: item.content, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/name_card.dart b/lib/widgets/name_card.dart index 1b32952..12e1f81 100644 --- a/lib/widgets/name_card.dart +++ b/lib/widgets/name_card.dart @@ -1,9 +1,5 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - -import '../auth.dart'; +import 'package:solaragent/auth.dart'; class NameCard extends StatelessWidget { const NameCard({super.key, this.onLogin, this.onCheck}); diff --git a/lib/widgets/navigation.dart b/lib/widgets/navigation.dart index 0761012..a5ba7a8 100644 --- a/lib/widgets/navigation.dart +++ b/lib/widgets/navigation.dart @@ -5,7 +5,7 @@ class AgentNavigation extends StatefulWidget { const AgentNavigation({super.key}); static const List<(String, NavigationDestination)> destinations = [ - ('/', NavigationDestination(icon: Icon(Icons.home), label: 'Home')), + ('/', NavigationDestination(icon: Icon(Icons.explore), label: 'Explore')), ('/notifications', NavigationDestination(icon: Icon(Icons.notifications), label: 'Notifications')), ('/account', NavigationDestination(icon: Icon(Icons.account_circle), label: 'Account')), ]; diff --git a/pubspec.lock b/pubspec.lock index ab1787c..16633c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,6 +142,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "87e11b9df25a42e2db315b8b7a51fae8e66f57a4b2f50ec4b822d0fa155e6b52" + url: "https://pub.dev" + source: hosted + version: "0.6.22" flutter_secure_storage: dependency: "direct main" description: @@ -190,6 +198,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + flutter_staggered_grid_view: + dependency: transitive + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_test: dependency: "direct dev" description: flutter @@ -232,6 +248,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.7" + infinite_scroll_pagination: + dependency: "direct main" + description: + name: infinite_scroll_pagination + sha256: b68bce20752fcf36c7739e60de4175494f74e99e9a69b4dd2fe3a1dd07a7f16a + url: "https://pub.dev" + source: hosted + version: "4.0.0" js: dependency: transitive description: @@ -288,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + markdown: + dependency: transitive + description: + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" + source: hosted + version: "7.2.2" matcher: dependency: transitive description: @@ -485,6 +517,14 @@ packages: description: flutter source: sdk version: "0.0.99" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" source_span: dependency: transitive description: @@ -695,4 +735,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.6" + flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 42eaade..d8653d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: package_info_plus: ^5.0.1 url_launcher: ^6.2.4 shared_preferences: ^2.2.2 + flutter_markdown: ^0.6.22 + infinite_scroll_pagination: ^4.0.0 dev_dependencies: flutter_test: