import 'package:flutter/material.dart'; import 'package:voyagehandbook/api/classes.dart'; import 'package:voyagehandbook/api/wikimedia.dart'; import 'package:voyagehandbook/util/storage.dart'; import 'package:voyagehandbook/views/pageview.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../util/drawer.dart'; /* Voyage Handbook - The open-source WikiVoyage reader Copyright (C) 2023 Matyáš Caras This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ class SearchView extends StatefulWidget { const SearchView({super.key}); @override State createState() => _SearchViewState(); } class _SearchViewState extends State { final _searchController = TextEditingController(); var _searchResults = []; var _showBox = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.searchAppBarTitle)), drawer: genDrawer(2, context), body: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Center( child: SizedBox( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width * 0.9, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextField( controller: _searchController, ), TextButton( onPressed: () async { if (_searchController.text == "") return; _showBox = true; setState(() {}); var r = await WikiApi.search(_searchController.text); _searchResults = r; setState(() {}); }, child: Text(AppLocalizations.of(context)!.search), ), const SizedBox( height: 15, ), AnimatedOpacity( opacity: (!_showBox) ? 0 : 1, duration: const Duration(milliseconds: 500), child: SizedBox( height: MediaQuery.of(context).size.height - 301, // TODO: maybe not responsive? width: MediaQuery.of(context).size.width * 0.9, child: Container( decoration: BoxDecoration( border: Border.all( color: Theme.of(context) .colorScheme .secondaryContainer, width: 1), borderRadius: BorderRadius.circular(4), ), child: (_searchResults.isNotEmpty) ? ListView.builder( itemBuilder: (context, index) { var span = []; var searchMatches = RegExp( r'.+?<\/span>') .allMatches(_searchResults[index].excerpt); if (searchMatches.isEmpty) { span = [ TextSpan( text: _searchResults[index].excerpt) ]; } else { // create emphasis on words matching search per the span element from API var text = _searchResults[index].excerpt; for (var match in searchMatches) { var split = text.split(match .group(0)!); // split by span element span.add(TextSpan( text: split[0])); // add text before span span.add( TextSpan( text: match.group(0)!.replaceAll( RegExp(r'<\/?span.*?>'), ""), style: const TextStyle( fontWeight: FontWeight.bold), ), ); // add span as bold text = text .replaceFirst( (split.isNotEmpty) ? split[0] : "", "") .replaceFirst(match.group(0)!, ""); // set text for next span to add } span.add( TextSpan(text: text), ); // add text we didn't add before } return Padding( padding: const EdgeInsets.only(left: 8, right: 8), child: SizedBox( height: 150, child: Card( elevation: 2, child: InkWell( onLongPress: () { // Show download dialog showDialog( context: context, builder: (_) => AlertDialog( title: Text( AppLocalizations.of(context)! .offlineTitle), content: Text( AppLocalizations.of(context)! .offlineDialog( _searchResults[index] .title), ), actions: [ TextButton( onPressed: () async { Navigator.of(context).pop(); showDialog( context: context, barrierDismissible: false, builder: (_) => Dialog( child: SizedBox( height: 100, child: Row( children: [ const Padding( padding: EdgeInsets .all( 10), child: CircularProgressIndicator(), ), Text(AppLocalizations .of(context)! .downloading) ], ), ), ), ); try { await StorageAccess .downloadArticle( _searchResults[ index] .key, _searchResults[ index] .title); if (!mounted) return; Navigator.of(context) .pop(); ScaffoldMessenger.of( context) .clearSnackBars(); ScaffoldMessenger.of( context) .showSnackBar( SnackBar( content: Text( AppLocalizations.of( context)! .downloadComplete), duration: const Duration( seconds: 4), ), ); } catch (e) { Navigator.of(context) .pop(); showDialog( context: context, builder: (_) => AlertDialog( title: Text( AppLocalizations.of( context)! .error), content: SingleChildScrollView( child: Text( e.toString()), ), actions: [ TextButton( onPressed: () => Navigator.of( context) .pop(), child: Text( AppLocalizations.of( context)! .ok), ) ], ), ); } }, child: Text( AppLocalizations.of( context)! .yes), ), TextButton( onPressed: () => Navigator.of(context) .pop(), child: Text( AppLocalizations.of( context)! .no), ), ], ), ); }, onTap: () { Navigator.of(context).push( MaterialPageRoute( builder: (c) => ArticleView( pageKey: _searchResults[index].key, name: _searchResults[index].title, ), ), ); }, child: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Row(children: [ Text( _searchResults[index].title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16), // TODO: responsive sizing ), if (_searchResults[index] .downloaded) const SizedBox( width: 15, ), if (_searchResults[index] .downloaded) const Icon( Icons.download, size: 15, ), ]), const SizedBox( height: 10, ), RichText( textAlign: TextAlign.justify, text: TextSpan(children: span), ), const SizedBox( height: 10, ) ], ), ), ), ), ), ); }, itemCount: _searchResults.length, ) : const SizedBox( height: 50, width: 50, child: CircularProgressIndicator(), ), ), ), ) ], ), ), ), ), ); } }