✨ Explore page
This commit is contained in:
parent
8bb9c960c1
commit
a3b4706ca2
@ -43,9 +43,11 @@ class SolarAgent extends StatelessWidget {
|
|||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(
|
OverlayEntry(
|
||||||
builder: (context) => SafeArea(
|
builder: (context) => SafeArea(
|
||||||
child: Scaffold(
|
child: SafeArea(
|
||||||
body: child,
|
child: Scaffold(
|
||||||
bottomNavigationBar: const AgentNavigation(),
|
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:go_router/go_router.dart';
|
||||||
import 'package:solaragent/screens/account.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';
|
import 'package:solaragent/screens/notifications.dart';
|
||||||
|
|
||||||
final router = GoRouter(
|
final router = GoRouter(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
builder: (context, state) => const DashboardScreen(),
|
builder: (context, state) => const ExploreScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/notifications',
|
path: '/notifications',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:solaragent/auth.dart';
|
import 'package:solaragent/auth.dart';
|
||||||
import 'package:solaragent/screens/about.dart';
|
import 'package:solaragent/screens/about.dart';
|
||||||
import 'package:solaragent/widgets/name_card.dart';
|
import 'package:solaragent/widgets/name_card.dart';
|
||||||
@ -27,72 +26,69 @@ class _AccountScreenState extends State<AccountScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
|
child: Column(children: [
|
||||||
child: Column(children: [
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(top: 20),
|
||||||
padding: const EdgeInsets.only(top: 20),
|
child: NameCard(
|
||||||
child: NameCard(
|
onLogin: () async {
|
||||||
onLogin: () async {
|
await authClient.signin(context);
|
||||||
await authClient.signin(context);
|
var authorized = await authClient.isAuthorized();
|
||||||
var authorized = await authClient.isAuthorized();
|
setState(() {
|
||||||
setState(() {
|
isAuthorized = authorized;
|
||||||
isAuthorized = authorized;
|
});
|
||||||
});
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
),
|
||||||
padding: const EdgeInsets.only(top: 5),
|
Padding(
|
||||||
child: Wrap(
|
padding: const EdgeInsets.only(top: 5),
|
||||||
spacing: 5,
|
child: Wrap(
|
||||||
children: [
|
spacing: 5,
|
||||||
FutureBuilder(
|
children: [
|
||||||
future: authClient.isAuthorized(),
|
FutureBuilder(
|
||||||
builder:
|
future: authClient.isAuthorized(),
|
||||||
(BuildContext context, AsyncSnapshot<bool> snapshot) {
|
builder:
|
||||||
return (snapshot.hasData && snapshot.data == true)
|
(BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||||
? InkWell(
|
return (snapshot.hasData && snapshot.data == true)
|
||||||
borderRadius:
|
? InkWell(
|
||||||
const BorderRadius.all(Radius.circular(40)),
|
borderRadius:
|
||||||
splashColor: Colors.indigo.withAlpha(30),
|
const BorderRadius.all(Radius.circular(40)),
|
||||||
onTap: () async {
|
splashColor: Colors.indigo.withAlpha(30),
|
||||||
authClient.signoff();
|
onTap: () async {
|
||||||
var authorized =
|
authClient.signoff();
|
||||||
await authClient.isAuthorized();
|
var authorized = await authClient.isAuthorized();
|
||||||
setState(() {
|
setState(() {
|
||||||
isAuthorized = authorized;
|
isAuthorized = authorized;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const ListTile(
|
child: const ListTile(
|
||||||
leading: Icon(Icons.logout),
|
leading: Icon(Icons.logout),
|
||||||
title: Text('Logout'),
|
title: Text('Logout'),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container();
|
: 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SafeArea(
|
body: RefreshIndicator(
|
||||||
child: RefreshIndicator(
|
onRefresh: pullNotifications,
|
||||||
onRefresh: pullNotifications,
|
child: CustomScrollView(
|
||||||
child: CustomScrollView(
|
slivers: [
|
||||||
slivers: [
|
// Title
|
||||||
// Title
|
SliverToBoxAdapter(
|
||||||
SliverToBoxAdapter(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 20),
|
child: ListTile(
|
||||||
child: ListTile(
|
title: Text(
|
||||||
title: Text(
|
'Notifications',
|
||||||
'Notifications',
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Content
|
),
|
||||||
notifications.isEmpty
|
// Content
|
||||||
? SliverToBoxAdapter(
|
notifications.isEmpty
|
||||||
child: Container(
|
? SliverToBoxAdapter(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
child: Container(
|
||||||
color: Colors.grey[300],
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: const ListTile(
|
color: Colors.grey[300],
|
||||||
leading: Icon(Icons.check),
|
child: const ListTile(
|
||||||
title: Text('You\'re done!'),
|
leading: Icon(Icons.check),
|
||||||
subtitle: Text(
|
title: Text('You\'re done!'),
|
||||||
'There are no notifications unread for you.',
|
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(
|
: SliverList.builder(
|
||||||
child: Padding(
|
itemCount: notifications.length,
|
||||||
padding: const EdgeInsets.only(top: 10),
|
itemBuilder: (BuildContext context, int index) {
|
||||||
child: Text(
|
var element = notifications[index];
|
||||||
"Pull to refresh, swipe to dismiss",
|
return Dismissible(
|
||||||
textAlign: TextAlign.center,
|
key: Key('notification-$index'),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
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/material.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:solaragent/auth.dart';
|
||||||
|
|
||||||
import '../auth.dart';
|
|
||||||
|
|
||||||
class NameCard extends StatelessWidget {
|
class NameCard extends StatelessWidget {
|
||||||
const NameCard({super.key, this.onLogin, this.onCheck});
|
const NameCard({super.key, this.onLogin, this.onCheck});
|
||||||
|
@ -5,7 +5,7 @@ class AgentNavigation extends StatefulWidget {
|
|||||||
const AgentNavigation({super.key});
|
const AgentNavigation({super.key});
|
||||||
|
|
||||||
static const List<(String, NavigationDestination)> destinations = [
|
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')),
|
('/notifications', NavigationDestination(icon: Icon(Icons.notifications), label: 'Notifications')),
|
||||||
('/account', NavigationDestination(icon: Icon(Icons.account_circle), label: 'Account')),
|
('/account', NavigationDestination(icon: Icon(Icons.account_circle), label: 'Account')),
|
||||||
];
|
];
|
||||||
|
42
pubspec.lock
42
pubspec.lock
@ -142,6 +142,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
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:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -190,6 +198,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -232,6 +248,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.7"
|
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:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -288,6 +312,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
markdown:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: markdown
|
||||||
|
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.2.2"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -485,6 +517,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -695,4 +735,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
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
|
package_info_plus: ^5.0.1
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
|
flutter_markdown: ^0.6.22
|
||||||
|
infinite_scroll_pagination: ^4.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user