✨ Explore page
This commit is contained in:
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
Reference in New Issue
Block a user