321 lines
10 KiB
Dart
321 lines
10 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:feet/notifications/tasks.dart';
|
|
import 'package:feet/widgets/centered_page_hint.dart';
|
|
import 'package:feet/widgets/filter_menu.dart';
|
|
import 'package:feet/widgets/homepage_post.dart';
|
|
import 'package:feet/widgets/login_prompt.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:dynamic_color/dynamic_color.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'api/api.dart';
|
|
|
|
// Limit the maximum amount of posts to fetch so we don't wait forever when
|
|
// fetching a large list
|
|
const FETCH_MAX_POSTS = 1000;
|
|
|
|
void main() {
|
|
setupBackgroundTasks();
|
|
runApp(MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
final api = FeverAPI();
|
|
SharedPreferences? prefs;
|
|
|
|
MyApp({super.key});
|
|
|
|
Future<bool> _loadPrefs() async {
|
|
var prefs = await SharedPreferences.getInstance();
|
|
this.prefs = prefs;
|
|
api.sharedPrefs = prefs;
|
|
var apiKey = prefs.getString('apiKey'), apiUrl = prefs.getString('apiUrl');
|
|
|
|
if (apiUrl != null && apiKey != null) {
|
|
api.apiUrl = apiUrl;
|
|
api.apiKey = apiKey;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DynamicColorBuilder(
|
|
builder: ((lightDynamic, darkDynamic) => MaterialApp(
|
|
title: 'Feet',
|
|
theme: ThemeData(
|
|
brightness: Brightness.light,
|
|
useMaterial3: true,
|
|
colorScheme: lightDynamic,
|
|
),
|
|
darkTheme: ThemeData(
|
|
brightness: Brightness.dark,
|
|
useMaterial3: true,
|
|
colorScheme: darkDynamic,
|
|
),
|
|
themeMode: ThemeMode.system,
|
|
home: FutureBuilder<bool>(
|
|
builder: (context, snapshot) {
|
|
if (snapshot.hasData && prefs != null) {
|
|
return MyHomePage(title: 'Feet', api: api, prefs: prefs!);
|
|
}
|
|
return const SizedBox();
|
|
},
|
|
future: _loadPrefs(),
|
|
),
|
|
)),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatefulWidget {
|
|
final String title;
|
|
final FeverAPI api;
|
|
final SharedPreferences prefs;
|
|
const MyHomePage(
|
|
{super.key, required this.title, required this.api, required this.prefs});
|
|
|
|
@override
|
|
State<MyHomePage> createState() => _MyHomePageState();
|
|
}
|
|
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|
var _index = 0;
|
|
var _fetched = false;
|
|
var _fetchedCount = 0;
|
|
var _knownPostsSize = -1;
|
|
List<int> _feedFilter = [];
|
|
|
|
Future<void> refresh() async {
|
|
widget.api.cache.clear();
|
|
setState(() {
|
|
_fetchedCount = 0;
|
|
_knownPostsSize = -1;
|
|
_feedFilter = [];
|
|
});
|
|
|
|
print("Starting refresh");
|
|
|
|
var data = await widget.api
|
|
.request()
|
|
.withFeeds()
|
|
.withGroups()
|
|
.withFavicons()
|
|
.withItems()
|
|
.sinceId(0)
|
|
.execute();
|
|
|
|
while ((data['items'] as List<dynamic>).isNotEmpty &&
|
|
_fetchedCount < FETCH_MAX_POSTS) {
|
|
print("Fetching more items");
|
|
|
|
var ids = widget.api.cache.items
|
|
.getAll()
|
|
.values
|
|
.map((value) => value.id)
|
|
.toList()
|
|
..sort((a, b) => b - a);
|
|
|
|
setState(() {
|
|
_fetchedCount = ids.length;
|
|
_knownPostsSize = data['total_items'] ?? -1;
|
|
});
|
|
|
|
data =
|
|
await widget.api.request().withItems().sinceId(ids.first).execute();
|
|
|
|
print(
|
|
"Item cache size is now ${widget.api.cache.items.size} ${ids.first} ${ids.last}");
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
refresh().then((_) => setState(() {
|
|
_fetched = true;
|
|
}));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var loggedIn = widget.api.loggedIn();
|
|
var posts = widget.api.cache.items.getAll().values.where((element) =>
|
|
_feedFilter.isEmpty || _feedFilter.contains(element.feedId));
|
|
|
|
var savedPosts = posts.where((element) => element.isSaved);
|
|
var unreadPosts = posts.where((element) => !element.isRead);
|
|
|
|
final pages = [
|
|
/* Posts list */
|
|
posts.isNotEmpty
|
|
? ListView(
|
|
children: posts.map((value) => HomepagePost(post: value)).toList()
|
|
..sort((a, b) => b.post.createdOnTime.millisecondsSinceEpoch
|
|
.compareTo(a.post.createdOnTime.millisecondsSinceEpoch)),
|
|
)
|
|
: const CenteredPageHint(
|
|
icon: Icons.coffee, text: "Your feed is currently empty."),
|
|
/* Unread */
|
|
unreadPosts.isNotEmpty
|
|
? ListView(
|
|
children: unreadPosts
|
|
.map((value) => HomepagePost(post: value))
|
|
.toList()
|
|
..sort((a, b) => b.post.createdOnTime.millisecondsSinceEpoch
|
|
.compareTo(a.post.createdOnTime.millisecondsSinceEpoch)),
|
|
)
|
|
: const CenteredPageHint(
|
|
icon: Icons.check, text: "Nothing new here!"),
|
|
/* Saved posts */
|
|
savedPosts.isNotEmpty
|
|
? ListView(
|
|
children: savedPosts
|
|
.map((value) => HomepagePost(post: value))
|
|
.toList()
|
|
..sort((a, b) => b.post.createdOnTime.millisecondsSinceEpoch
|
|
.compareTo(a.post.createdOnTime.millisecondsSinceEpoch)),
|
|
)
|
|
: const CenteredPageHint(
|
|
icon: Icons.bookmarks_outlined,
|
|
text: "Nothing here yet. Try saving some posts!"),
|
|
];
|
|
|
|
// I'm aware that this is stupid, but I'm sick and tired and
|
|
// really don't care as long as it works
|
|
if (!loggedIn) {
|
|
Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
setState(() {
|
|
loggedIn = widget.api.loggedIn();
|
|
});
|
|
|
|
if (loggedIn) {
|
|
timer.cancel();
|
|
refresh().then((_) => setState(() => _fetched = true));
|
|
}
|
|
});
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(widget.title),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () {
|
|
var theme = Theme.of(context);
|
|
|
|
HapticFeedback.heavyImpact();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Refreshing feeds',
|
|
style: TextStyle(color: theme.colorScheme.onBackground)),
|
|
duration: const Duration(seconds: 2),
|
|
backgroundColor: theme.colorScheme.secondaryContainer,
|
|
),
|
|
);
|
|
refresh().then((_) {
|
|
setState(() {});
|
|
});
|
|
},
|
|
icon: const Icon(Icons.replay_outlined),
|
|
), // Refresh
|
|
PopupMenuButton(
|
|
itemBuilder: (context) => [
|
|
PopupMenuItem(
|
|
value: "log_out",
|
|
child: const Text("Log out"),
|
|
onTap: () {
|
|
Future.delayed(
|
|
const Duration(seconds: 0),
|
|
() => showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text("Log out"),
|
|
content: const Text(
|
|
"Are you sure you want to log out?"),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
await widget.prefs.clear();
|
|
|
|
setState(() {
|
|
widget.api.apiKey = null;
|
|
widget.api.apiUrl = null;
|
|
widget.api.cache.clear();
|
|
});
|
|
},
|
|
child: const Text("Do it!")),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
child: const Text("Nevermind"),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
PopupMenuItem(
|
|
value: "filter",
|
|
child: Text(_feedFilter.isEmpty
|
|
? "Filter..."
|
|
: "Filter (${_feedFilter.length}/${widget.api.cache.feeds.size})"),
|
|
onTap: () {
|
|
Future.delayed(
|
|
Duration.zero,
|
|
() => showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => FilterMenu(
|
|
api: widget.api,
|
|
initData: _feedFilter,
|
|
onConfirm: (items) {
|
|
setState(() {
|
|
_feedFilter = items;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
bottomNavigationBar: BottomNavigationBar(
|
|
currentIndex: _index,
|
|
onTap: (value) => setState(() => _index = value),
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.list),
|
|
label: 'Feed',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.lens_blur),
|
|
label: 'Unread',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.bookmark_outline),
|
|
activeIcon: Icon(Icons.bookmark),
|
|
label: 'Saved',
|
|
),
|
|
],
|
|
),
|
|
body: loggedIn
|
|
? (_fetched
|
|
? pages[_index]
|
|
: CenteredPageHint(
|
|
icon: Icons.downloading,
|
|
text:
|
|
"Fetching your feeds...\n${_fetchedCount > 0 && _knownPostsSize > -1 ? "$_fetchedCount / $_knownPostsSize" : ""}"))
|
|
: Center(child: LoginPrompt(api: widget.api)),
|
|
);
|
|
}
|
|
}
|