basic api access, state managment etc

This commit is contained in:
Mike Schwörer 2024-02-11 01:08:51 +01:00
parent 306d9a006a
commit 46897cc51b
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
16 changed files with 431 additions and 50 deletions

View File

@ -1,4 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="simplecloudnotifier"
android:name="${applicationName}"

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();
}
}

View File

@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@ -82,6 +82,22 @@ packages:
relative: true
source: path
version: "10.7.0"
http:
dependency: "direct main"
description:
name: http
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
url: "https://pub.dev"
source: hosted
version: "1.2.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
lints:
dependency: transitive
description:
@ -114,6 +130,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
dependency: transitive
description:
@ -122,6 +146,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.3"
provider:
dependency: "direct main"
description:
name: provider
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
sky_engine:
dependency: transitive
description: flutter
@ -175,6 +207,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
vector_math:
dependency: transitive
description:
@ -193,3 +233,4 @@ packages:
version: "0.3.0"
sdks:
dart: ">=3.2.6 <4.0.0"
flutter: ">=1.16.0"

View File

@ -36,6 +36,8 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
http: ^1.2.0
provider: ^6.1.1
dependency_overrides:

View File

@ -1,30 +0,0 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:simplecloudnotifier/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}