basic api access, state managment etc

This commit is contained in:
2024-02-11 01:08:51 +01:00
parent 306d9a006a
commit 46897cc51b
16 changed files with 431 additions and 50 deletions

View File

@@ -0,0 +1,12 @@
import 'package:http/http.dart' as http;
class APIClient {
static const String _base = 'https://simplecloudnotifier.de/api/v2';
static Future<bool> verifyToken(String uid, String tok) async {
final uri = Uri.parse('$_base/users/$uid');
final response = await http.get(uri, headers: {'Authorization': 'SCN $tok'});
return (response.statusCode == 200);
}
}

View File

@@ -1,24 +1,36 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'nav_layout.dart';
import 'state/app_theme.dart';
import 'state/user_account.dart';
void main() {
runApp(const SCNApp());
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UserAccount()),
ChangeNotifierProvider(create: (context) => AppTheme()),
],
child: const SCNApp(),
),
);
}
class SCNApp extends StatelessWidget {
const SCNApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SimpleCloudNotifier',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
return Consumer<AppTheme>(
builder: (context, appTheme, child) => MaterialApp(
title: 'SimpleCloudNotifier',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
useMaterial3: true,
),
home: const SCNNavLayout(),
),
home: const SCNNavLayout(),
);
}
}

View File

@@ -0,0 +1,6 @@
class KeyTokenAuth {
final String userId;
final String token;
KeyTokenAuth({required this.userId, required this.token});
}

View File

@@ -0,0 +1,87 @@
class User {
final String userID;
final String? username;
final String timestampCreated;
final String? timestampLastRead;
final String? timestampLastSent;
final int messagesSent;
final int quotaUsed;
final int quotaRemaining;
final int quotaPerDay;
final bool isPro;
final String defaultChannel;
final int maxBodySize;
final int maxTitleLength;
final int defaultPriority;
final int maxChannelNameLength;
final int maxChannelDescriptionLength;
final int maxSenderNameLength;
final int maxUserMessageIDLength;
const User({
required this.userID,
required this.username,
required this.timestampCreated,
required this.timestampLastRead,
required this.timestampLastSent,
required this.messagesSent,
required this.quotaUsed,
required this.quotaRemaining,
required this.quotaPerDay,
required this.isPro,
required this.defaultChannel,
required this.maxBodySize,
required this.maxTitleLength,
required this.defaultPriority,
required this.maxChannelNameLength,
required this.maxChannelDescriptionLength,
required this.maxSenderNameLength,
required this.maxUserMessageIDLength,
});
factory User.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'user_id': String userID,
'username': String? username,
'timestamp_created': String timestampCreated,
'timestamp_lastread': String? timestampLastRead,
'timestamp_lastsent': String? timestampLastSent,
'messages_sent': int messagesSent,
'quota_used': int quotaUsed,
'quota_remaining': int quotaRemaining,
'quota_max': int quotaPerDay,
'is_pro': bool isPro,
'default_channel': String defaultChannel,
'max_body_size': int maxBodySize,
'max_title_length': int maxTitleLength,
'default_priority': int defaultPriority,
'max_channel_name_length': int maxChannelNameLength,
'max_channel_description_length': int maxChannelDescriptionLength,
'max_sender_name_length': int maxSenderNameLength,
'max_user_message_id_length': int maxUserMessageIDLength,
} =>
User(
userID: userID,
username: username,
timestampCreated: timestampCreated,
timestampLastRead: timestampLastRead,
timestampLastSent: timestampLastSent,
messagesSent: messagesSent,
quotaUsed: quotaUsed,
quotaRemaining: quotaRemaining,
quotaPerDay: quotaPerDay,
isPro: isPro,
defaultChannel: defaultChannel,
maxBodySize: maxBodySize,
maxTitleLength: maxTitleLength,
defaultPriority: defaultPriority,
maxChannelNameLength: maxChannelNameLength,
maxChannelDescriptionLength: maxChannelDescriptionLength,
maxSenderNameLength: maxSenderNameLength,
maxUserMessageIDLength: maxUserMessageIDLength,
),
_ => throw const FormatException('Failed to decode User.'),
};
}
}

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'bottom_fab/fab_bottom_app_bar.dart';
import 'pages/account/root.dart';
import 'pages/message_list/message_list.dart';
import 'state/app_theme.dart';
class SCNNavLayout extends StatefulWidget {
const SCNNavLayout({super.key});
@@ -14,13 +17,11 @@ class SCNNavLayout extends StatefulWidget {
class _SCNNavLayoutState extends State<SCNNavLayout> {
int _selectedIndex = 0;
static const TextStyle optionStyle = TextStyle(fontSize: 30, fontWeight: FontWeight.bold);
static const List<Widget> _subPages = <Widget>[
MessageListPage(title: 'Messages 1'),
MessageListPage(title: 'Messages 2'),
MessageListPage(title: 'Messages 3'),
MessageListPage(title: 'Messages 4'),
MessageListPage(title: 'Messages'),
MessageListPage(title: 'Page 2'),
AccountRootPage(),
MessageListPage(title: 'Page 4'),
];
void _onItemTapped(int index) {
@@ -39,9 +40,7 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(context),
body: Center(
child: _subPages.elementAt(_selectedIndex),
),
body: _subPages.elementAt(_selectedIndex),
bottomNavigationBar: _buildNavBar(context),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
floatingActionButton: _buildFAB(context),
@@ -61,8 +60,8 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
Widget _buildNavBar(BuildContext context) {
return FABBottomAppBar(
onTabSelected: _onItemTapped,
color: Colors.grey,
selectedColor: Colors.black,
color: Theme.of(context).disabledColor,
selectedColor: Theme.of(context).primaryColorDark,
notchedShape: const AutomaticNotchedShape(
RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@@ -83,10 +82,19 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
);
}
AppBar _buildAppBar(BuildContext context) {
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
title: const Text('Simple Cloud Notifier 2.0'),
actions: <Widget>[
Consumer<AppTheme>(
builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: 'Debug',
onPressed: () {
appTheme.switchDarkMode();
},
),
),
IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class AccountChoosePage extends StatelessWidget {
final void Function()? onLogin;
final void Function()? onCreateAccount;
const AccountChoosePage({super.key, this.onLogin, this.onCreateAccount});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () {
onLogin?.call();
},
child: const Text('Use existing account'),
),
const SizedBox(height: 32),
ElevatedButton(
style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: () {
onCreateAccount?.call();
},
child: const Text('Create new account'),
),
],
),
);
}
}

View File

@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
class AccountLoginPage extends StatefulWidget {
const AccountLoginPage({super.key});
@override
State<AccountLoginPage> createState() => _AccountLoginPageState();
}
class _AccountLoginPageState extends State<AccountLoginPage> {
late TextEditingController _ctrlUserID;
late TextEditingController _ctrlToken;
@override
void initState() {
super.initState();
_ctrlUserID = TextEditingController();
_ctrlToken = TextEditingController();
}
@override
void dispose() {
_ctrlUserID.dispose();
_ctrlToken.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 250,
child: TextField(
controller: _ctrlUserID,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'UserID',
),
),
),
const SizedBox(height: 16),
SizedBox(
width: 250,
child: TextField(
controller: _ctrlToken,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Token',
),
),
),
const SizedBox(height: 16),
ElevatedButton(
style: ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20)),
onPressed: _login,
child: const Text('Login'),
),
],
),
);
}
void _login() async {
final msgr = ScaffoldMessenger.of(context);
final verified = await APIClient.verifyToken(_ctrlUserID.text, _ctrlToken.text);
if (verified) {
msgr.showSnackBar(
const SnackBar(
content: Text('Data ok'),
),
);
} else {
msgr.showSnackBar(
const SnackBar(
content: Text('Failed to verify token'),
),
);
}
}
}

View File

@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/pages/account/login.dart';
import '../../state/user_account.dart';
import 'choose_auth.dart';
class AccountRootPage extends StatefulWidget {
const AccountRootPage({super.key});
@override
State<AccountRootPage> createState() => _AccountRootPageState();
}
enum _SubPage { chooseAuth, login, main }
class _AccountRootPageState extends State<AccountRootPage> {
late _SubPage _page;
@override
void initState() {
super.initState();
var prov = Provider.of<UserAccount>(context, listen: false);
_page = (prov.auth != null) ? _SubPage.main : _SubPage.chooseAuth;
prov.addListener(_onAuthStateChanged);
}
@override
void dispose() {
Provider.of<UserAccount>(context, listen: false).removeListener(_onAuthStateChanged);
super.dispose();
}
void _onAuthStateChanged() {
if (Provider.of<UserAccount>(context, listen: false).auth != null && _page != _SubPage.main) {
setState(() {
_page = _SubPage.main;
});
}
}
@override
Widget build(BuildContext context) {
return Consumer<UserAccount>(
builder: (context, acc, child) {
switch (_page) {
case _SubPage.main:
return const Center(
child: Text(
'Logged In',
style: TextStyle(fontSize: 24),
),
);
case _SubPage.chooseAuth:
return AccountChoosePage(
onLogin: () => setState(() {
_page = _SubPage.login;
}),
onCreateAccount: () => setState(() {
//TODO
}),
);
case _SubPage.login:
return const AccountLoginPage();
}
},
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/foundation.dart';
class AppTheme extends ChangeNotifier {
bool _darkmode = false;
bool get darkMode => _darkmode;
void setDarkMode(bool v) {
_darkmode = v;
notifyListeners();
}
void switchDarkMode() {
_darkmode = !_darkmode;
notifyListeners();
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter/foundation.dart';
import '../models/key_token_auth.dart';
import '../models/user.dart';
class UserAccount extends ChangeNotifier {
User? _user;
User? get user => _user;
KeyTokenAuth? _auth;
KeyTokenAuth? get auth => _auth;
void setToken(KeyTokenAuth auth) {
_auth = auth;
_user = user;
notifyListeners();
}
void setUser(User user) {
_user = user;
notifyListeners();
}
void clearUser() {
_user = null;
notifyListeners();
}
}