import '../../state/app_state.dart'; import 'package:pb_authenticator_state/state.dart'; import 'package:flutter/material.dart'; import '../../helper/url.dart' show launchURL; import '/l10n/app_localizations.dart'; import 'package:provider/provider.dart'; import '../../config/routes.dart'; import '../../l10n/constants.dart'; import './list_item.dart' show HomeListItem; enum SortOption { nameAsc, nameDesc } /// Android version of home page. class AndroidHomePage extends StatefulWidget { const AndroidHomePage({super.key}); @override State createState() => _AndroidHomePageState(); } class _AndroidHomePageState extends State { String _searchQuery = ''; SortOption _currentSort = SortOption.nameAsc; final TextEditingController _searchController = TextEditingController(); bool _isSearchVisible = false; @override void dispose() { _searchController.dispose(); super.dispose(); } void _toggleSearch() { setState(() { _isSearchVisible = !_isSearchVisible; if (!_isSearchVisible) { // Clear search when hiding _searchQuery = ''; _searchController.clear(); } }); } List _filterAndSortItems(List items) { // Filter by search query List filteredItems = items; if (_searchQuery.isNotEmpty) { filteredItems = items.where((item) { final nameMatch = item.totp.accountName.toLowerCase().contains(_searchQuery.toLowerCase()); final issuerMatch = item.totp.issuer.toLowerCase().contains(_searchQuery.toLowerCase()); return nameMatch || issuerMatch; }).toList(); } // Sort items filteredItems.sort((a, b) { String aName = a.totp.accountName.isNotEmpty ? a.totp.accountName : (a.totp.issuer.isNotEmpty ? a.totp.issuer : 'Unknown'); String bName = b.totp.accountName.isNotEmpty ? b.totp.accountName : (b.totp.issuer.isNotEmpty ? b.totp.issuer : 'Unknown'); if (_currentSort == SortOption.nameAsc) { return aName.toLowerCase().compareTo(bName.toLowerCase()); } else { return bName.toLowerCase().compareTo(aName.toLowerCase()); } }); return filteredItems; } /// Builds search and sort controls. Widget _buildSearchAndSort() { return Container( padding: const EdgeInsets.all(16.0), child: Column( children: [ // Search field TextField( controller: _searchController, decoration: InputDecoration( hintText: AppLocalizations.of(context)!.searchHint, prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { _searchQuery = ''; _searchController.clear(); }); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), onChanged: (value) { setState(() { _searchQuery = value; }); }, ), const SizedBox(height: 12), // Sort options Row( children: [ Text( AppLocalizations.of(context)!.sortBy, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(width: 16), Expanded( child: Row( children: [ Expanded( child: RadioListTile( title: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.arrow_upward, size: 16), const SizedBox(width: 4), Text(AppLocalizations.of(context)!.sortAscending), ], ), value: SortOption.nameAsc, groupValue: _currentSort, onChanged: (SortOption? value) { if (value != null) { setState(() { _currentSort = value; }); } }, contentPadding: EdgeInsets.zero, dense: true, ), ), Expanded( child: RadioListTile( title: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.arrow_downward, size: 16), const SizedBox(width: 4), Text(AppLocalizations.of(context)!.sortDescending), ], ), value: SortOption.nameDesc, groupValue: _currentSort, onChanged: (SortOption? value) { if (value != null) { setState(() { _currentSort = value; }); } }, contentPadding: EdgeInsets.zero, dense: true, ), ), ], ), ), ], ), ], ), ); } /// Builds list of items. Widget _buildList() { return Consumer( builder: (context, state, child) { if (state.items == null) { // Items loading return Center( child: Padding( padding: const EdgeInsets.all(10), child: Text(AppLocalizations.of(context)!.loading))); } else if (state.items!.isEmpty) { // No Items return Center( child: Padding( padding: const EdgeInsets.all(10), child: Text(AppLocalizations.of(context)!.noAccounts, textAlign: TextAlign.center, style: const TextStyle(height: 1.2)))); } final filteredAndSortedItems = _filterAndSortItems(state.items!); if (filteredAndSortedItems.isEmpty && _searchQuery.isNotEmpty) { // No search results return Center( child: Padding( padding: const EdgeInsets.all(10), child: Text(AppLocalizations.of(context)!.noSearchResults(_searchQuery), textAlign: TextAlign.center, style: const TextStyle(height: 1.2)))); } return Container( margin: const EdgeInsets.all(10), child: ListView.builder( itemCount: filteredAndSortedItems.length, itemBuilder: (BuildContext context, int index) { var item = filteredAndSortedItems[index]; return HomeListItem(item, key: Key(item.id)); }, ), ); }, ); } /// Shows add modal void showAddModal(BuildContext parentContext) { // Show bottom sheet showModalBottomSheet( context: parentContext, builder: (BuildContext context) { return Container( padding: const EdgeInsets.all(8.0), child: ListView( shrinkWrap: true, children: [ // Scan QR ListTile( title: Text(AppLocalizations.of(context)!.addScanQR), leading: const Icon(Icons.camera_alt), onTap: () async { Navigator.pop(context); var result = await Navigator.pushNamed(context, AppRoutes.addScan); if (result == null) { return; } // TODO: Transition to new type var item = result as LegacyAuthenticatorItem; await Provider.of(parentContext, listen: false) .addItem(item.totp); }, ), // Add account manually ListTile( title: Text(AppLocalizations.of(context)!.addManualInput), leading: const Icon(Icons.keyboard_return), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.add); }, ), ], ), ); }, ); } @override Widget build(BuildContext context) { return Scaffold( drawer: Drawer( child: ListView( padding: EdgeInsets.zero, children: [ DrawerHeader( decoration: BoxDecoration( color: Theme.of(context).primaryColor, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( AppLocalizations.of(context)!.appName, style: TextStyle( color: Colors.white, fontSize: Theme.of(context).textTheme.titleLarge!.fontSize), ), ], ), ), // Transfer codes ListTile( title: Text(AppLocalizations.of(context)!.transferCodesTitle ?? 'Transfer codes'), leading: const Icon(Icons.sync_alt), dense: true, onTap: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.transferCodes); }, ), // How it works ListTile( title: Text(AppLocalizations.of(context)!.howItWorksTitle ?? 'How it works'), leading: const Icon(Icons.info_outline), dense: true, onTap: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.howItWorks); }, ), // Settings ListTile( title: Text(AppLocalizations.of(context)!.settingsTitle), leading: const Icon(Icons.settings), dense: true, onTap: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.settings); } ), const Divider(height: 10), // Source code (in-app browser link) ListTile( title: Text(AppLocalizations.of(context)!.source), leading: const Icon(Icons.code), dense: true, onTap: () { Navigator.pop(context); launchURL(Constants.repoUrl); }), // Help ListTile( title: Text(AppLocalizations.of(context)!.helpTitle ?? 'Help'), leading: const Icon(Icons.help_outline), dense: true, onTap: () { Navigator.pop(context); Navigator.pushNamed(context, AppRoutes.help); }, ), ], ), ), appBar: AppBar( title: Text(AppLocalizations.of(context)!.appName), actions: [ // Edit IconButton( icon: const Icon(Icons.edit), tooltip: AppLocalizations.of(context)!.edit, onPressed: () { Navigator.pushNamed(context, AppRoutes.edit); }, ), // Search IconButton( icon: Icon(_isSearchVisible ? Icons.search_off : Icons.search), tooltip: AppLocalizations.of(context)!.search, onPressed: _toggleSearch, ), // Add IconButton( icon: const Icon(Icons.add), tooltip: AppLocalizations.of(context)!.add, onPressed: () { showAddModal(context); }, ) ], ), body: Column( children: [ if (_isSearchVisible) _buildSearchAndSort(), Expanded(child: _buildList()), ], ), ); } }