voyagehandbook/lib/views/search.dart

191 lines
8.7 KiB
Dart
Raw Normal View History

2023-03-18 16:55:08 +01:00
import 'package:flutter/material.dart';
2023-03-24 23:48:55 +01:00
import 'package:voyagehandbook/api/classes.dart';
2023-03-18 16:55:08 +01:00
import 'package:voyagehandbook/api/wikimedia.dart';
2023-03-24 23:48:55 +01:00
import 'package:voyagehandbook/views/pageview.dart';
2023-03-18 16:55:08 +01:00
import '../util/drawer.dart';
2023-03-28 18:08:26 +02:00
/*
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 as published by
the Free Software Foundation, either version 3 of the License.
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/>.
*/
2023-03-18 16:55:08 +01:00
class SearchView extends StatefulWidget {
const SearchView({super.key});
@override
State<SearchView> createState() => _SearchViewState();
}
class _SearchViewState extends State<SearchView> {
final _searchController = TextEditingController();
2023-03-24 23:48:55 +01:00
var _searchResults = <SearchResponse>[];
var _showBox = false;
2023-03-18 16:55:08 +01:00
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Search WikiVoyage")),
drawer: genDrawer(2, context),
2023-03-24 23:48:55 +01:00
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(
2023-03-18 16:55:08 +01:00
onPressed: () async {
if (_searchController.text == "") return;
2023-03-24 23:48:55 +01:00
_showBox = true;
setState(() {});
2023-03-18 16:55:08 +01:00
var r = await WikiApi.search(_searchController.text);
2023-03-24 23:48:55 +01:00
_searchResults = r;
setState(() {});
2023-03-18 16:55:08 +01:00
},
2023-03-24 23:48:55 +01:00
child: const Text("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(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => ArticleView(
2023-03-27 19:45:57 +02:00
pageKey:
_searchResults[index].key,
name:
_searchResults[index].title,
),
2023-03-24 23:48:55 +01:00
),
);
},
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(),
),
),
),
)
],
),
2023-03-18 16:55:08 +01:00
),
),
),
);
}
}