voyagehandbook/lib/views/search.dart
2023-04-06 22:46:40 +02:00

210 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:voyagehandbook/api/classes.dart';
import 'package:voyagehandbook/api/wikimedia.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 <https://www.gnu.org/licenses/>.
*/
class SearchView extends StatefulWidget {
const SearchView({super.key});
@override
State<SearchView> createState() => _SearchViewState();
}
class _SearchViewState extends State<SearchView> {
final _searchController = TextEditingController();
var _searchResults = <SearchResponse>[];
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 = <TextSpan>[];
var searchMatches = RegExp(
r'<span class="searchmatch">.+?<\/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),
),
),
);
},
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: [
Text(
_searchResults[index].title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize:
16), // TODO: responsive sizing
),
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(),
),
),
),
)
],
),
),
),
),
);
}
}