import 'dart:async'; import 'package:feet/notifications/tasks.dart'; import 'package:feet/util.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 _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( 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 createState() => _MyHomePageState(); } class _MyHomePageState extends State { var _index = 0; var _fetched = false; var _fetchedCount = 0; var _knownPostsSize = -1; var _isDemoUser = false; List _feedFilter = []; Future 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).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(); setState(() { _isDemoUser = widget.prefs.getBool("isDemo") ?? false; }); 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: [ _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(), 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; }); }, ), ), ); }, ), PopupMenuItem( value: "feedback", child: const Text("Feedback"), onTap: () async { await openUrl(context, "https://git.amogus.cloud/Lea/feet/issues"); }, ), ]), ], ), 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)), ); } }