♻️ Refactored and joined the Solar Network
This commit is contained in:
@ -3,34 +3,33 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:goatagent/firebase.dart';
|
||||
import 'package:goatagent/screens/auth.dart';
|
||||
import 'package:solaragent/firebase.dart';
|
||||
import 'package:solaragent/preferences.dart';
|
||||
import 'package:solaragent/screens/auth.dart';
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
|
||||
final authClient = AuthGuard();
|
||||
|
||||
class AuthGuard {
|
||||
static final AuthGuard _singleton = AuthGuard._internal();
|
||||
AuthGuard();
|
||||
|
||||
final deviceEndpoint =
|
||||
Uri.parse('https://id.smartsheep.studio/api/notifications/subscribe');
|
||||
Uri.parse('https://id.solsynth.dev/api/notifications/subscribe');
|
||||
final authorizationEndpoint =
|
||||
Uri.parse('https://id.smartsheep.studio/auth/o/connect');
|
||||
Uri.parse('https://id.solsynth.dev/auth/o/connect');
|
||||
final tokenEndpoint =
|
||||
Uri.parse('https://id.smartsheep.studio/api/auth/token');
|
||||
Uri.parse('https://id.solsynth.dev/api/auth/token');
|
||||
final userinfoEndpoint =
|
||||
Uri.parse('https://id.smartsheep.studio/api/users/me');
|
||||
final redirectUrl = Uri.parse('goatagent://auth');
|
||||
Uri.parse('https://id.solsynth.dev/api/users/me');
|
||||
final redirectUrl = Uri.parse('solaragent://auth');
|
||||
|
||||
static const clientId = "goatagent";
|
||||
static const clientId = "solaragent";
|
||||
static const clientSecret = "_F4%q2Eea3";
|
||||
|
||||
static const storage = FlutterSecureStorage();
|
||||
static const storageKey = "identity";
|
||||
static const profileKey = "profiles";
|
||||
|
||||
factory AuthGuard() {
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
oauth2.Client? client;
|
||||
|
||||
Future<bool> pickClient() async {
|
||||
@ -68,11 +67,6 @@ class AuthGuard {
|
||||
var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
// Let Goatpass know it is embed in an app
|
||||
authorizationUrl = authorizationUrl.replace(
|
||||
queryParameters: {"embedded": "yes"}
|
||||
..addAll(authorizationUrl.queryParameters));
|
||||
|
||||
// Use WebView to get authorization url
|
||||
var responseUrl = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
|
@ -67,7 +67,7 @@ class DefaultFirebaseOptions {
|
||||
messagingSenderId: '659822066072',
|
||||
projectId: 'smartsheep-hydrogen',
|
||||
storageBucket: 'smartsheep-hydrogen.appspot.com',
|
||||
iosBundleId: 'studio.smartsheep.goatagent',
|
||||
iosBundleId: 'dev.solsynth.solaragent',
|
||||
);
|
||||
|
||||
static const FirebaseOptions macos = FirebaseOptions(
|
||||
@ -76,6 +76,6 @@ class DefaultFirebaseOptions {
|
||||
messagingSenderId: '659822066072',
|
||||
projectId: 'smartsheep-hydrogen',
|
||||
storageBucket: 'smartsheep-hydrogen.appspot.com',
|
||||
iosBundleId: 'studio.smartsheep.goatagent.RunnerTests',
|
||||
iosBundleId: 'dev.solsynth.solaragent.RunnerTests',
|
||||
);
|
||||
}
|
||||
|
@ -1,114 +0,0 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:goatagent/screens/account.dart';
|
||||
import 'package:goatagent/screens/dashboard.dart';
|
||||
import 'package:goatagent/screens/notifications.dart';
|
||||
|
||||
class AgentNavigation extends StatefulWidget {
|
||||
AgentNavigation({super.key});
|
||||
|
||||
static const items = [
|
||||
{'label': 'Dashboard', 'icon': Icon(Icons.home)},
|
||||
{'label': 'Notifications', 'icon': Icon(Icons.notifications)},
|
||||
{'label': 'Account', 'icon': Icon(Icons.account_circle)},
|
||||
];
|
||||
|
||||
final bottomDestinations = items
|
||||
.map((element) => BottomNavigationBarItem(
|
||||
icon: element["icon"] as Widget,
|
||||
label: element["label"] as String,
|
||||
))
|
||||
.toList();
|
||||
final railDestinations = items
|
||||
.map((element) => NavigationRailDestination(
|
||||
icon: element["icon"] as Widget,
|
||||
label: Text(element["label"] as String),
|
||||
))
|
||||
.toList();
|
||||
|
||||
@override
|
||||
State<AgentNavigation> createState() => _AgentNavigationState();
|
||||
}
|
||||
|
||||
class _AgentNavigationState extends State<AgentNavigation> {
|
||||
int _view = 0;
|
||||
|
||||
Future<void> initMessage(BuildContext context) async {
|
||||
void navigate() {
|
||||
setState(() {
|
||||
_view = 1;
|
||||
});
|
||||
}
|
||||
|
||||
RemoteMessage? initialMessage =
|
||||
await FirebaseMessaging.instance.getInitialMessage();
|
||||
|
||||
if (initialMessage != null) {
|
||||
navigate();
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((event) {
|
||||
navigate();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initMessage(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isLargeScreen = MediaQuery.of(context).size.width > 640;
|
||||
|
||||
final content = Stack(
|
||||
children: [
|
||||
Offstage(
|
||||
offstage: _view != 0,
|
||||
child: const DashboardScreen(),
|
||||
),
|
||||
Offstage(
|
||||
offstage: _view != 1,
|
||||
child: const NotificationScreen(),
|
||||
),
|
||||
Offstage(
|
||||
offstage: _view != 2,
|
||||
child: const AccountScreen(),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
if (isLargeScreen) {
|
||||
return Row(children: <Widget>[
|
||||
NavigationRail(
|
||||
selectedIndex: _view,
|
||||
groupAlignment: 0,
|
||||
onDestinationSelected: (int index) {
|
||||
setState(() {
|
||||
_view = index;
|
||||
});
|
||||
},
|
||||
labelType: NavigationRailLabelType.all,
|
||||
destinations: widget.railDestinations,
|
||||
),
|
||||
const VerticalDivider(thickness: 1, width: 1),
|
||||
Expanded(child: content),
|
||||
]);
|
||||
} else {
|
||||
return Scaffold(
|
||||
body: content,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _view,
|
||||
showUnselectedLabels: false,
|
||||
items: widget.bottomDestinations,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_view = index;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:goatagent/auth.dart';
|
||||
import 'package:goatagent/firebase.dart';
|
||||
|
||||
import 'layouts/navigation.dart';
|
||||
import 'package:solaragent/auth.dart';
|
||||
import 'package:solaragent/firebase.dart';
|
||||
import 'package:solaragent/router.dart';
|
||||
import 'package:solaragent/widgets/navigation.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@ -13,39 +13,51 @@ void main() async {
|
||||
print(e);
|
||||
}
|
||||
|
||||
await AuthGuard().pickClient();
|
||||
await authClient.pickClient();
|
||||
|
||||
runApp(const GoatAgent());
|
||||
runApp(const SolarAgent());
|
||||
}
|
||||
|
||||
class GoatAgent extends StatelessWidget {
|
||||
const GoatAgent({super.key});
|
||||
class SolarAgent extends StatelessWidget {
|
||||
const SolarAgent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'GoatAgent',
|
||||
return MaterialApp.router(
|
||||
title: 'SolarAgent',
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.indigo,
|
||||
accentColor: Colors.indigoAccent,
|
||||
backgroundColor: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.indigo,
|
||||
accentColor: Colors.indigoAccent,
|
||||
backgroundColor: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.indigo,
|
||||
accentColor: Colors.indigoAccent,
|
||||
backgroundColor: Colors.black,
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.indigo,
|
||||
accentColor: Colors.indigoAccent,
|
||||
backgroundColor: Colors.black,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: AgentNavigation(),
|
||||
routerConfig: router,
|
||||
builder: (context, child) => Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
builder: (context) => SafeArea(
|
||||
child: Scaffold(
|
||||
body: child,
|
||||
bottomNavigationBar: const AgentNavigation(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
5
lib/preferences.dart
Normal file
5
lib/preferences.dart
Normal file
@ -0,0 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
getPreferences() async {
|
||||
return await SharedPreferences.getInstance();
|
||||
}
|
21
lib/router.dart
Normal file
21
lib/router.dart
Normal file
@ -0,0 +1,21 @@
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solaragent/screens/account.dart';
|
||||
import 'package:solaragent/screens/dashboard.dart';
|
||||
import 'package:solaragent/screens/notifications.dart';
|
||||
|
||||
final router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const DashboardScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications',
|
||||
builder: (context, state) => const NotificationScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
)
|
||||
],
|
||||
);
|
@ -16,9 +16,9 @@ class AboutScreen extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('GoatAgent',
|
||||
Text('SolarAgent',
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
Text('Goatworks Official Mobile Helper',
|
||||
Text('Solarworks Official Mobile Helper',
|
||||
style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 20),
|
||||
FutureBuilder(
|
||||
@ -37,7 +37,7 @@ class AboutScreen extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
MaterialButton(
|
||||
onPressed: () async {
|
||||
await launchUrl(Uri.parse('https://smartsheep.studio'));
|
||||
await launchUrl(Uri.parse('https://solsynth.dev'));
|
||||
},
|
||||
child: const Text('Official Website'),
|
||||
),
|
||||
|
@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:goatagent/auth.dart';
|
||||
import 'package:goatagent/screens/about.dart';
|
||||
import 'package:goatagent/widgets/name_card.dart';
|
||||
import 'package:solaragent/auth.dart';
|
||||
import 'package:solaragent/screens/about.dart';
|
||||
import 'package:solaragent/widgets/name_card.dart';
|
||||
|
||||
class AccountScreen extends StatefulWidget {
|
||||
const AccountScreen({super.key});
|
||||
@ -17,7 +17,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AuthGuard().isAuthorized().then((value) {
|
||||
authClient.isAuthorized().then((value) {
|
||||
setState(() {
|
||||
isAuthorized = value;
|
||||
});
|
||||
@ -35,8 +35,8 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: NameCard(
|
||||
onLogin: () async {
|
||||
await AuthGuard().login(context);
|
||||
var authorized = await AuthGuard().isAuthorized();
|
||||
await authClient.login(context);
|
||||
var authorized = await authClient.isAuthorized();
|
||||
setState(() {
|
||||
isAuthorized = authorized;
|
||||
});
|
||||
@ -49,44 +49,43 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
spacing: 5,
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: AuthGuard().isAuthorized(),
|
||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||
if (snapshot.hasData && snapshot.data == true) {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
splashColor: Colors.indigo.withAlpha(30),
|
||||
onTap: () async {
|
||||
AuthGuard().logout();
|
||||
var authorized = await AuthGuard().isAuthorized();
|
||||
setState(() {
|
||||
isAuthorized = authorized;
|
||||
});
|
||||
},
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text('Logout'),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
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.logout();
|
||||
var authorized =
|
||||
await authClient.isAuthorized();
|
||||
setState(() {
|
||||
isAuthorized = authorized;
|
||||
});
|
||||
},
|
||||
child: const ListTile(
|
||||
leading: Icon(Icons.logout),
|
||||
title: Text('Logout'),
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
},
|
||||
),
|
||||
Card(
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -11,7 +11,7 @@ class AuthorizationScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Connect with Goatpass'),
|
||||
title: const Text('Sign in'),
|
||||
),
|
||||
body: Stack(children: [
|
||||
WebViewWidget(
|
||||
@ -20,10 +20,10 @@ class AuthorizationScreen extends StatelessWidget {
|
||||
..setBackgroundColor(Colors.white)
|
||||
..setNavigationDelegate(NavigationDelegate(
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
if (request.url.startsWith('goatagent://auth')) {
|
||||
if (request.url.startsWith('solaragent://auth')) {
|
||||
Navigator.of(context).pop(request.url);
|
||||
return NavigationDecision.prevent;
|
||||
} else if (request.url.startsWith("https://id.smartsheep.studio/auth/register")) {
|
||||
} else if (request.url.startsWith("https://solsynth.dev/auth/sign-up")) {
|
||||
launchUrl(Uri.parse(request.url));
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:goatagent/screens/application.dart';
|
||||
import 'package:solaragent/screens/application.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
@ -14,7 +14,7 @@ class DashboardScreen extends StatefulWidget {
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
final client = http.Client();
|
||||
final directoryEndpoint =
|
||||
Uri.parse('https://id.smartsheep.studio/.well-known');
|
||||
Uri.parse('https://id.solsynth.dev/.well-known');
|
||||
|
||||
List<dynamic> directory = List.empty();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:goatagent/auth.dart';
|
||||
import 'package:solaragent/auth.dart';
|
||||
|
||||
class NotificationScreen extends StatefulWidget {
|
||||
const NotificationScreen({super.key});
|
||||
@ -12,7 +12,7 @@ class NotificationScreen extends StatefulWidget {
|
||||
|
||||
class _NotificationScreenState extends State<NotificationScreen> {
|
||||
final notificationEndpoint = Uri.parse(
|
||||
'https://id.smartsheep.studio/api/notifications?skip=0&take=20');
|
||||
'https://id.solsynth.dev/api/notifications?skip=0&take=25');
|
||||
|
||||
List<dynamic> notifications = List.empty();
|
||||
|
||||
@ -23,9 +23,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
}
|
||||
|
||||
Future<void> _pullNotifications() async {
|
||||
if (await AuthGuard().isAuthorized()) {
|
||||
await AuthGuard().pullProfiles();
|
||||
var profiles = await AuthGuard().readProfiles();
|
||||
if (await authClient.isAuthorized()) {
|
||||
await authClient.pullProfiles();
|
||||
var profiles = await authClient.readProfiles();
|
||||
setState(() {
|
||||
notifications = profiles['notifications'];
|
||||
});
|
||||
@ -33,11 +33,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
}
|
||||
|
||||
Future<void> _markAsRead(element) async {
|
||||
if (AuthGuard().client != null) {
|
||||
if (authClient.client != null) {
|
||||
var id = element['id'];
|
||||
var uri =
|
||||
Uri.parse('https://id.smartsheep.studio/api/notifications/$id/read');
|
||||
await AuthGuard().client!.put(uri);
|
||||
Uri.parse('https://id.solsynth.dev/api/notifications/$id/read');
|
||||
await authClient.client!.put(uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,8 @@ class NameCard extends StatelessWidget {
|
||||
final void Function()? onCheck;
|
||||
|
||||
Future<CircleAvatar> _getAvatar() async {
|
||||
if (await AuthGuard().isAuthorized()) {
|
||||
final profiles = await AuthGuard().readProfiles();
|
||||
if (await authClient.isAuthorized()) {
|
||||
final profiles = await authClient.readProfiles();
|
||||
return CircleAvatar(backgroundImage: NetworkImage(profiles["picture"]));
|
||||
} else {
|
||||
return const CircleAvatar(child: Icon(Icons.account_circle));
|
||||
@ -21,8 +21,8 @@ class NameCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<Column> _getDescribe() async {
|
||||
if (await AuthGuard().isAuthorized()) {
|
||||
final profiles = await AuthGuard().readProfiles();
|
||||
if (await authClient.isAuthorized()) {
|
||||
final profiles = await authClient.readProfiles();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -59,7 +59,7 @@ class NameCard extends StatelessWidget {
|
||||
child: InkWell(
|
||||
splashColor: Colors.indigo.withAlpha(30),
|
||||
onTap: () async {
|
||||
if (await AuthGuard().isAuthorized() && onCheck != null) {
|
||||
if (await authClient.isAuthorized() && onCheck != null) {
|
||||
onCheck!();
|
||||
} else if (onLogin != null) {
|
||||
onLogin!();
|
||||
|
38
lib/widgets/navigation.dart
Normal file
38
lib/widgets/navigation.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:solaragent/router.dart';
|
||||
|
||||
class AgentNavigation extends StatefulWidget {
|
||||
const AgentNavigation({super.key});
|
||||
|
||||
static const List<(String, NavigationDestination)> destinations = [
|
||||
('/', NavigationDestination(icon: Icon(Icons.home), label: 'Home')),
|
||||
('/notifications', NavigationDestination(icon: Icon(Icons.notifications), label: 'Notifications')),
|
||||
('/account', NavigationDestination(icon: Icon(Icons.account_circle), label: 'Account')),
|
||||
];
|
||||
|
||||
@override
|
||||
State<AgentNavigation> createState() => _AgentNavigationState();
|
||||
}
|
||||
|
||||
class _AgentNavigationState extends State<AgentNavigation> {
|
||||
int currentPage = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigationBar(
|
||||
selectedIndex: currentPage,
|
||||
destinations: AgentNavigation.destinations
|
||||
.map(((String, NavigationDestination) e) => e.$2)
|
||||
.toList(),
|
||||
onDestinationSelected: (index) {
|
||||
router.go(AgentNavigation.destinations[index].$1);
|
||||
setState(() => currentPage = index);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user