feet/lib/main.dart

377 lines
12 KiB
Dart
Raw Normal View History

2022-12-29 20:43:02 +00:00
import 'dart:async';
import 'package:feet/notifications/tasks.dart';
import 'package:feet/util.dart';
2023-01-01 01:01:50 +00:00
import 'package:feet/widgets/centered_page_hint.dart';
2023-01-23 09:45:52 +00:00
import 'package:feet/widgets/filter_menu.dart';
2022-12-29 23:27:35 +00:00
import 'package:feet/widgets/homepage_post.dart';
2022-12-29 18:16:46 +00:00
import 'package:feet/widgets/login_prompt.dart';
2022-12-29 14:39:31 +00:00
import 'package:flutter/material.dart';
2022-12-29 18:41:45 +00:00
import 'package:dynamic_color/dynamic_color.dart';
2023-01-01 01:19:24 +00:00
import 'package:flutter/services.dart';
2022-12-29 20:43:02 +00:00
import 'package:shared_preferences/shared_preferences.dart';
2022-12-29 18:16:46 +00:00
import 'api/api.dart';
2022-12-29 14:39:31 +00:00
// Limit the maximum amount of posts to fetch so we don't wait forever when
// fetching a large list
const FETCH_MAX_POSTS = 1000;
2022-12-29 14:39:31 +00:00
void main() {
setupBackgroundTasks();
2022-12-29 18:16:46 +00:00
runApp(MyApp());
2022-12-29 14:39:31 +00:00
}
class MyApp extends StatelessWidget {
2022-12-29 18:16:46 +00:00
final api = FeverAPI();
2022-12-29 20:43:02 +00:00
SharedPreferences? prefs;
2022-12-29 18:16:46 +00:00
MyApp({super.key});
2022-12-29 14:39:31 +00:00
2022-12-29 20:43:02 +00:00
Future<bool> _loadPrefs() async {
var prefs = await SharedPreferences.getInstance();
this.prefs = prefs;
api.sharedPrefs = prefs;
2023-01-01 01:01:50 +00:00
var apiKey = prefs.getString('apiKey'), apiUrl = prefs.getString('apiUrl');
2022-12-29 20:43:02 +00:00
if (apiUrl != null && apiKey != null) {
api.apiUrl = apiUrl;
api.apiKey = apiKey;
return true;
}
return false;
}
2022-12-29 14:39:31 +00:00
@override
Widget build(BuildContext context) {
2022-12-29 18:41:45 +00:00
return DynamicColorBuilder(
builder: ((lightDynamic, darkDynamic) => MaterialApp(
2023-01-01 01:01:50 +00:00
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(),
),
)),
2022-12-29 14:39:31 +00:00
);
}
}
class MyHomePage extends StatefulWidget {
final String title;
2022-12-29 18:16:46 +00:00
final FeverAPI api;
2022-12-29 20:43:02 +00:00
final SharedPreferences prefs;
2023-01-01 01:01:50 +00:00
const MyHomePage(
{super.key, required this.title, required this.api, required this.prefs});
2022-12-29 14:39:31 +00:00
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
2023-01-01 01:01:50 +00:00
var _index = 0;
2023-01-01 01:19:24 +00:00
var _fetched = false;
var _fetchedCount = 0;
var _knownPostsSize = -1;
var _isDemoUser = false;
2023-01-23 09:45:52 +00:00
List<int> _feedFilter = [];
Future<void> refresh() async {
widget.api.cache.clear();
2023-01-23 09:45:52 +00:00
setState(() {
_fetchedCount = 0;
_knownPostsSize = -1;
_feedFilter = [];
});
print("Starting refresh");
var data = await widget.api
.request()
.withFeeds()
.withGroups()
2023-01-30 07:28:21 +00:00
.withFavicons()
.withItems()
.sinceId(0)
.execute();
2023-01-23 09:45:52 +00:00
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);
2023-01-01 01:19:24 +00:00
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}");
}
}
2023-01-01 01:19:24 +00:00
@override
void initState() {
super.initState();
setState(() {
_isDemoUser = widget.prefs.getBool("isDemo") ?? false;
});
2023-01-01 01:19:24 +00:00
refresh().then((_) => setState(() {
_fetched = true;
}));
}
2023-01-01 01:01:50 +00:00
2022-12-29 14:39:31 +00:00
@override
Widget build(BuildContext context) {
2022-12-29 20:43:02 +00:00
var loggedIn = widget.api.loggedIn();
2023-01-30 07:28:21 +00:00
var posts = widget.api.cache.items.getAll().values.where((element) =>
_feedFilter.isEmpty || _feedFilter.contains(element.feedId));
2023-01-23 09:45:52 +00:00
2023-01-01 01:01:50 +00:00
var savedPosts = posts.where((element) => element.isSaved);
var unreadPosts = posts.where((element) => !element.isRead);
final pages = [
/* Posts list */
posts.isNotEmpty
2023-01-01 01:19:24 +00:00
? 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."),
2023-01-01 01:01:50 +00:00
/* Unread */
unreadPosts.isNotEmpty
2023-01-01 01:19:24 +00:00
? 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!"),
2023-01-01 01:01:50 +00:00
/* Saved posts */
savedPosts.isNotEmpty
2023-01-01 01:19:24 +00:00
? 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!"),
2023-01-01 01:01:50 +00:00
];
2022-12-29 20:43:02 +00:00
2023-01-23 09:45:52 +00:00
// I'm aware that this is stupid, but I'm sick and tired and
// really don't care as long as it works
2022-12-29 20:43:02 +00:00
if (!loggedIn) {
Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
loggedIn = widget.api.loggedIn();
});
2023-01-01 01:24:20 +00:00
if (loggedIn) {
timer.cancel();
refresh().then((_) => setState(() => _fetched = true));
}
2022-12-29 20:43:02 +00:00
});
}
2022-12-29 14:39:31 +00:00
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
2022-12-29 23:27:35 +00:00
actions: [
_isDemoUser
? ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Connected to demo server"),
content: RichText(text: TextSpan(
children: [
const TextSpan(
text: "You are currently connected to the demo server.\n"
"Use the"
),
WidgetSpan(child: Icon(Icons.adaptive.more, size: 20),
),
const TextSpan(
text: "button in the top right to log out and switch "
"to your own instance once you're done exploring."
),
],
)),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("Got it!"),
),
],
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 114, 77, 7),
foregroundColor: Colors.amber,
),
child: Row(children: const [
Icon(Icons.warning),
SizedBox(width: 8),
Text("Demo"),
])
)
: Container(),
2022-12-29 23:27:35 +00:00
IconButton(
onPressed: () {
2023-01-01 01:19:24 +00:00
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((_) {
2023-01-01 01:01:50 +00:00
setState(() {});
});
2022-12-29 23:27:35 +00:00
},
icon: const Icon(Icons.replay_outlined),
2023-01-16 09:59:35 +00:00
), // 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"),
),
],
),
),
);
},
2023-01-23 09:45:52 +00:00
),
PopupMenuItem(
value: "filter",
2023-01-30 07:28:21 +00:00
child: Text(_feedFilter.isEmpty
2023-01-23 09:45:52 +00:00
? "Filter..."
2023-01-30 07:28:21 +00:00
: "Filter (${_feedFilter.length}/${widget.api.cache.feeds.size})"),
2023-01-23 09:45:52 +00:00
onTap: () {
Future.delayed(
Duration.zero,
() => showDialog(
context: context,
barrierDismissible: false,
builder: (context) => FilterMenu(
api: widget.api,
initData: _feedFilter,
onConfirm: (items) {
setState(() {
_feedFilter = items;
});
},
),
),
);
},
),
PopupMenuItem(
value: "feedback",
child: const Text("Feedback"),
onTap: () async {
await openUrl(context,
"https://git.amogus.cloud/Lea/feet/issues");
},
),
2023-01-16 09:59:35 +00:00
]),
2022-12-29 23:27:35 +00:00
],
2022-12-29 14:39:31 +00:00
),
2023-01-01 01:01:50 +00:00
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',
),
],
),
2022-12-29 20:43:02 +00:00
body: loggedIn
2023-01-01 01:19:24 +00:00
? (_fetched
? pages[_index]
: CenteredPageHint(
icon: Icons.downloading,
text:
"Fetching your feeds...\n${_fetchedCount > 0 && _knownPostsSize > -1 ? "$_fetchedCount / $_knownPostsSize" : ""}"))
2023-01-01 01:01:50 +00:00
: Center(child: LoginPrompt(api: widget.api)),
2022-12-29 14:39:31 +00:00
);
}
}