something something login screen

This commit is contained in:
Jan 2022-12-29 19:16:46 +01:00
parent 380868c126
commit 3a8fdd27ad
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
5 changed files with 161 additions and 109 deletions

View file

@ -9,6 +9,7 @@ part 'feed.dart';
part 'item.dart'; part 'item.dart';
part 'group.dart'; part 'group.dart';
part 'feeds_group.dart'; part 'feeds_group.dart';
part 'cache.dart';
part 'exceptions.dart'; part 'exceptions.dart';
part 'util.dart'; part 'util.dart';
@ -16,19 +17,27 @@ part 'util.dart';
/// https://github.com/dasmurphy/tinytinyrss-fever-plugin/blob/master/fever-api.md /// https://github.com/dasmurphy/tinytinyrss-fever-plugin/blob/master/fever-api.md
class FeverAPI { class FeverAPI {
String apiKey; String? apiKey;
String apiUrl; String? apiUrl;
/// We use a shared HTTP client for all requests /// We use a shared HTTP client for all requests
final _httpClient = http.Client(); final _httpClient = http.Client();
FeverAPI({ required this.apiKey, required this.apiUrl }); late final APICache cache;
FeverAPI({ this.apiKey, this.apiUrl }) {
cache = APICache(this);
}
static String generateApiKey(String username, String password) { static String generateApiKey(String username, String password) {
var bytes = utf8.encode('$username:$password'); var bytes = utf8.encode('$username:$password');
return md5.convert(bytes).toString(); return md5.convert(bytes).toString();
} }
bool loggedIn() {
return apiKey != null && apiUrl != null;
}
APIRequestBuilder request() { APIRequestBuilder request() {
return APIRequestBuilder(this, _httpClient); return APIRequestBuilder(this, _httpClient);
} }
@ -70,6 +79,8 @@ class APIRequestBuilder {
/// Executes the API request and returns the JSON data /// Executes the API request and returns the JSON data
Future<Map<String, dynamic>> fetch() async { Future<Map<String, dynamic>> fetch() async {
if (api.apiKey == null || api.apiUrl == null) throw Exception('API Key or API URL not set');
final response = await _httpClient.post(generateUrl(), body: { 'api_key': api.apiKey }); final response = await _httpClient.post(generateUrl(), body: { 'api_key': api.apiKey });
final data = jsonDecode((response).body); final data = jsonDecode((response).body);
if (data['auth'] == 0) throw UnauthenticatedException(data['auth']); if (data['auth'] == 0) throw UnauthenticatedException(data['auth']);
@ -80,48 +91,43 @@ class APIRequestBuilder {
Future<void> execute() async { Future<void> execute() async {
final data = await fetch(); final data = await fetch();
List<Group> groups = [];
List<Feed> feeds = [];
List<Item> items = [];
List<FeedsGroup> feedsGroups = [];
if (data.containsKey('groups')) { if (data.containsKey('groups')) {
groups = (data['groups'] as List<dynamic>) List<Group> groups = (data['groups'] as List<dynamic>)
.map((json) => Group.fromJSON(api, json)) .map((json) => Group.fromJSON(api, json))
.toList(); .toList();
for (var group in groups) {
api.cache.groups.set(group.id, group);
}
} }
if (data.containsKey('feeds')) { if (data.containsKey('feeds')) {
feeds = (data['feeds'] as List<dynamic>) List<Feed> feeds = (data['feeds'] as List<dynamic>)
.map((json) => Feed.fromJSON(api, json)) .map((json) => Feed.fromJSON(api, json))
.toList(); .toList();
for (var feed in feeds) {
api.cache.feeds.set(feed.id, feed);
}
} }
if (data.containsKey('items')) { if (data.containsKey('items')) {
items = (data['items'] as List<dynamic>) List<Item> items = (data['items'] as List<dynamic>)
.map((json) => Item.fromJSON(api, json)) .map((json) => Item.fromJSON(api, json))
.toList(); .toList();
for (var item in items) {
api.cache.items.set(item.id, item);
}
} }
if (data.containsKey('feeds_groups')) { if (data.containsKey('feeds_groups')) {
feedsGroups = (data['feeds_groups'] as List<dynamic>) List<FeedsGroup> feedsGroups = (data['feeds_groups'] as List<dynamic>)
.map((json) => FeedsGroup.fromJSON(api, json)) .map((json) => FeedsGroup.fromJSON(api, json))
.toList(); .toList();
}
print(groups); api.cache.feedsGroups.clear();
print(feeds); api.cache.feedsGroups.addAll(feedsGroups);
print(items); }
print(feedsGroups);
} }
} }
void main() {
var api = FeverAPI(apiKey: "", apiUrl: "https://rss.amogus.cloud/api/fever.php");
api.request()
.withFeeds()
.withGroups()
.withItems()
.execute();
}

48
lib/api/cache.dart Normal file
View file

@ -0,0 +1,48 @@
part of fever_api;
/// Caches items, feeds and groups
class APICache {
final FeverAPI api;
late final ItemCache items;
late final ObjectCache<int, Feed> feeds;
late final ObjectCache<int, Group> groups;
final List<FeedsGroup> feedsGroups = [];
APICache(this.api) {
items = ItemCache(api);
feeds = ObjectCache(api);
groups = ObjectCache(api);
}
}
class ObjectCache<K, T> {
final FeverAPI api;
final Map<K, T> _items = {};
ObjectCache(this.api);
/// Returns an item from the cache, or null if it doesn't exist
T? get(K key) {
return _items.containsKey(key) ? _items[key] : null;
}
/// Returns a copy of the cache content
Map<K, T> getAll() {
return {..._items};
}
void set(K key, T value) {
_items[key] = value;
}
}
class ItemCache extends ObjectCache<int, Item> {
/// Get all items for a given feed
List<Item> getForFeed(int feed) {
if (api.cache.feeds.get(feed) == null) throw Exception('Feed $feed doesn\'t exist');
return _items.values.where((item) => item.feedId == feed).toList();
}
ItemCache(FeverAPI api) : super(api);
}

View file

@ -1,115 +1,53 @@
import 'package:feet/widgets/login_prompt.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'api/api.dart';
void main() { void main() {
runApp(const MyApp()); runApp(MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); final api = FeverAPI();
MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Feet',
theme: ThemeData( theme: ThemeData(
// This is the theme of your application. brightness: Brightness.light,
// useMaterial3: true,
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
), ),
home: const MyHomePage(title: 'Flutter Demo Home Page'), darkTheme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
),
themeMode: ThemeMode.system,
home: MyHomePage(title: 'Feet', api: api),
); );
} }
} }
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title}); MyHomePage({ super.key, required this.title, required this.api });
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title; final String title;
final FeverAPI api;
@override @override
State<MyHomePage> createState() => _MyHomePageState(); State<MyHomePage> createState() => _MyHomePageState();
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title), title: Text(widget.title),
), ),
body: Center( body: widget.api.loggedIn()
// Center is a layout widget. It takes a single child and positions it ? const Center(child: Text('Meep'))
// in the middle of the parent. : const Center(child: LoginPrompt()),
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
); );
} }
} }

39
lib/pages/login.dart Normal file
View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
LoginPage({ super.key });
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Log in'),
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text("Fever compatible API URL"),
TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.all(8.0),
hintText: 'https://among-us.morbius.sus/api/fever.php',
)
)
],
),
)
],
),
);
}
}

View file

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:feet/pages/login.dart';
class LoginPrompt extends StatelessWidget {
const LoginPrompt({ super.key });
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text('Please log in'),
ElevatedButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => LoginPage()));
},
child: const Text('Log in')
),
],
);
}
}