|
@@ -8,10 +8,159 @@ import '../../config/routes.dart';
|
|
|
import '../../l10n/constants.dart';
|
|
import '../../l10n/constants.dart';
|
|
|
import './list_item.dart' show HomeListItem;
|
|
import './list_item.dart' show HomeListItem;
|
|
|
|
|
|
|
|
|
|
+enum SortOption { nameAsc, nameDesc }
|
|
|
|
|
+
|
|
|
/// Android version of home page.
|
|
/// Android version of home page.
|
|
|
-class AndroidHomePage extends StatelessWidget {
|
|
|
|
|
|
|
+class AndroidHomePage extends StatefulWidget {
|
|
|
const AndroidHomePage({super.key});
|
|
const AndroidHomePage({super.key});
|
|
|
|
|
|
|
|
|
|
+ @override
|
|
|
|
|
+ State<AndroidHomePage> createState() => _AndroidHomePageState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class _AndroidHomePageState extends State<AndroidHomePage> {
|
|
|
|
|
+ String _searchQuery = '';
|
|
|
|
|
+ SortOption _currentSort = SortOption.nameAsc;
|
|
|
|
|
+ final TextEditingController _searchController = TextEditingController();
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ void dispose() {
|
|
|
|
|
+ _searchController.dispose();
|
|
|
|
|
+ super.dispose();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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.
|
|
/// Builds list of items.
|
|
|
Widget _buildList() {
|
|
Widget _buildList() {
|
|
|
return Consumer<AppState>(
|
|
return Consumer<AppState>(
|
|
@@ -32,12 +181,24 @@ class AndroidHomePage extends StatelessWidget {
|
|
|
style: const TextStyle(height: 1.2))));
|
|
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(
|
|
return Container(
|
|
|
margin: const EdgeInsets.all(10),
|
|
margin: const EdgeInsets.all(10),
|
|
|
child: ListView.builder(
|
|
child: ListView.builder(
|
|
|
- itemCount: state.items!.length,
|
|
|
|
|
|
|
+ itemCount: filteredAndSortedItems.length,
|
|
|
itemBuilder: (BuildContext context, int index) {
|
|
itemBuilder: (BuildContext context, int index) {
|
|
|
- var item = state.items![index];
|
|
|
|
|
|
|
+ var item = filteredAndSortedItems[index];
|
|
|
return HomeListItem(item, key: Key(item.id));
|
|
return HomeListItem(item, key: Key(item.id));
|
|
|
},
|
|
},
|
|
|
),
|
|
),
|
|
@@ -190,7 +351,12 @@ class AndroidHomePage extends StatelessWidget {
|
|
|
)
|
|
)
|
|
|
],
|
|
],
|
|
|
),
|
|
),
|
|
|
- body: _buildList(),
|
|
|
|
|
|
|
+ body: Column(
|
|
|
|
|
+ children: [
|
|
|
|
|
+ _buildSearchAndSort(),
|
|
|
|
|
+ Expanded(child: _buildList()),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|