✨ Explore page
This commit is contained in:
		@@ -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(),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										59
									
								
								lib/models/author.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								lib/models/author.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<String, dynamic> 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<String, dynamic> 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,
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										154
									
								
								lib/models/feed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								lib/models/feed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<Attachment>? attachments;
 | 
			
		||||
  Map<String, dynamic>? 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<String, dynamic> 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<Attachment>.from(
 | 
			
		||||
                json["attachments"].map((x) => Attachment.fromJson(x)))
 | 
			
		||||
            : List.empty(),
 | 
			
		||||
        reactionList: json["reaction_list"],
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> 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<dynamic>.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<String, dynamic> 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<String, dynamic> 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,
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								lib/models/pagination.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/models/pagination.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
class PaginationResult {
 | 
			
		||||
  int count;
 | 
			
		||||
  List<dynamic>? data;
 | 
			
		||||
 | 
			
		||||
  PaginationResult({
 | 
			
		||||
    required this.count,
 | 
			
		||||
    this.data,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  factory PaginationResult.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      PaginationResult(count: json["count"], data: json["data"]);
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> toJson() => {
 | 
			
		||||
        "count": count,
 | 
			
		||||
        "data": data,
 | 
			
		||||
      };
 | 
			
		||||
}
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -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<AccountScreen> {
 | 
			
		||||
  @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<bool> 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<bool> 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'),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ]),
 | 
			
		||||
        ),
 | 
			
		||||
          ),
 | 
			
		||||
        ]),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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(),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<DashboardScreen> createState() => _DashboardScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _DashboardScreenState extends State<DashboardScreen> {
 | 
			
		||||
  final client = http.Client();
 | 
			
		||||
  final directoryEndpoint =
 | 
			
		||||
      Uri.parse('https://id.solsynth.dev/.well-known');
 | 
			
		||||
 | 
			
		||||
  List<dynamic> directory = List.empty();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _pullDirectory();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _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(),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								lib/screens/explore.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/screens/explore.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<ExploreScreen> createState() => _ExploreScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
  static const pageSize = 5;
 | 
			
		||||
 | 
			
		||||
  final client = http.Client();
 | 
			
		||||
 | 
			
		||||
  final PagingController<int, Feed> paginationController =
 | 
			
		||||
      PagingController(firstPageKey: 0);
 | 
			
		||||
 | 
			
		||||
  List<Feed> feed = List.empty();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    paginationController.addPageRequestListener((pageKey) {
 | 
			
		||||
      pullFeed(pageKey);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> 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<int, Feed>(
 | 
			
		||||
        pagingController: paginationController,
 | 
			
		||||
        builderDelegate: PagedChildBuilderDelegate<Feed>(
 | 
			
		||||
          itemBuilder: (context, item, index) => FeedItem(
 | 
			
		||||
            item: item,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    paginationController.dispose();
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -46,94 +46,92 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
  @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,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								lib/widgets/feed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								lib/widgets/feed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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});
 | 
			
		||||
 
 | 
			
		||||
@@ -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')),
 | 
			
		||||
  ];
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user