| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- 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<AndroidHomePage> createState() => _AndroidHomePageState();
- }
- class _AndroidHomePageState extends State<AndroidHomePage> {
- 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<BaseItemType> _filterAndSortItems(List<BaseItemType> items) {
- // Filter by search query
- List<BaseItemType> 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<SortOption>(
- 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<SortOption>(
- 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<AppState>(
- 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<void>(
- context: parentContext,
- builder: (BuildContext context) {
- return Container(
- padding: const EdgeInsets.all(8.0),
- child: ListView(
- shrinkWrap: true,
- children: <Widget>[
- // 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<AppState>(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: <Widget>[
- 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: <Widget>[
- // 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()),
- ],
- ),
- );
- }
- }
|