v1.1.12+42
All checks were successful
Flutter Schmutter / build (push) Successful in 3m45s

- search schmearch
This commit is contained in:
Flummi 2025-06-10 11:07:00 +02:00
parent c35308fbc1
commit 5876c809a5
5 changed files with 146 additions and 78 deletions

View File

@ -3,12 +3,16 @@ class Suggestion {
final int tagged; final int tagged;
final double score; final double score;
Suggestion({required this.tag, required this.tagged, required this.score}); Suggestion({
required this.tag,
required this.tagged,
required this.score,
});
factory Suggestion.fromJson(Map<String, dynamic> json) { factory Suggestion.fromJson(Map<String, dynamic> json) {
return Suggestion( return Suggestion(
tag: json['tag'].toString(), tag: json['tag'].toString(),
tagged: int.tryParse(json['tagged'].toString()) ?? 0, tagged: json['tagged'],
score: (json['score'] as num).toDouble(), score: (json['score'] as num).toDouble(),
); );
} }

View File

@ -4,14 +4,12 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/providers/media_provider.dart'; import 'package:f0ckapp/providers/media_provider.dart';
import 'package:f0ckapp/utils/appversion_util.dart'; import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart'; import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/utils/customsearchdelegate_util.dart'; import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
class MediaGrid extends ConsumerStatefulWidget { class MediaGrid extends ConsumerStatefulWidget {
const MediaGrid({super.key}); const MediaGrid({super.key});
@ -56,8 +54,8 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider); final MediaState mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier); final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
@ -80,14 +78,12 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.search), icon: const Icon(Icons.search),
onPressed: () { onPressed: () async {
showSearch( await showSearch(
context: context, context: context,
delegate: CustomSearchDelegate(), delegate: CustomSearchDelegate(),
); );
//mediaNotifier.setTag('drachenlord');
//_scrollController.jumpTo(0);
}, },
), ),
IconButton( IconButton(
@ -163,7 +159,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
child: null, child: null,
), ),
ExpansionTile( /*ExpansionTile(
title: const Text('Login'), title: const Text('Login'),
children: [ children: [
Padding( Padding(
@ -193,7 +189,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
const SnackBar( const SnackBar(
content: Text("noch nicht implementiert lol"), content: Text("noch nicht implementiert lol"),
), ),
/*final success = await login( final success = await login(
_usernameController.text, _usernameController.text,
_passwordController.text, _passwordController.text,
); );
@ -204,7 +200,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login fehlgeschlagen!")), SnackBar(content: Text("Login fehlgeschlagen!")),
); );
}*/ }
); );
}, },
child: const Text('Login'), child: const Text('Login'),
@ -213,7 +209,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
), ),
], ],
), ),*/
ExpansionTile( ExpansionTile(
title: const Text('Theme'), title: const Text('Theme'),
children: [ children: [
@ -221,10 +217,10 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: themeMap.entries.map((entry) { children: themeMap.entries.map((entry) {
final themeName = entry.key; final String themeName = entry.key;
final themeData = entry.value; final ThemeData themeData = entry.value;
final currentTheme = ref.watch(themeNotifierProvider); final ThemeData currentTheme = ref.watch(themeNotifierProvider);
final isSelected = currentTheme == themeData; final bool isSelected = currentTheme == themeData;
return ListTile( return ListTile(
title: Text(themeName), title: Text(themeName),
selected: isSelected, selected: isSelected,
@ -284,11 +280,11 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
itemCount: itemCount:
mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0), mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (BuildContext context, int index) {
if (index >= mediaState.mediaItems.length) { if (index >= mediaState.mediaItems.length) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
final item = mediaState.mediaItems[index]; final MediaItem item = mediaState.mediaItems[index];
return InkWell( return InkWell(
onTap: () async { onTap: () async {

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -6,7 +7,9 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/mediaitem_model.dart'; import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/models/suggestion_model.dart'; import 'package:f0ckapp/models/suggestion_model.dart';
final FlutterSecureStorage storage = FlutterSecureStorage(); final FlutterSecureStorage storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
Future<List<MediaItem>> fetchMedia({ Future<List<MediaItem>> fetchMedia({
int? older, int? older,
@ -25,7 +28,7 @@ Future<List<MediaItem>> fetchMedia({
}, },
); );
final response = await http.get(url); final http.Response response = await http.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body); final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((item) => MediaItem.fromJson(item)).toList(); return jsonList.map((item) => MediaItem.fromJson(item)).toList();
@ -37,7 +40,7 @@ Future<List<MediaItem>> fetchMedia({
Future<MediaItem> fetchMediaDetail(int itemId) async { Future<MediaItem> fetchMediaDetail(int itemId) async {
final Uri url = Uri.parse('https://api.f0ck.me/item/${itemId.toString()}'); final Uri url = Uri.parse('https://api.f0ck.me/item/${itemId.toString()}');
final response = await http.get(url); final http.Response response = await http.get(url);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(response.body); final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
@ -50,39 +53,45 @@ Future<MediaItem> fetchMediaDetail(int itemId) async {
} }
Future<List<Suggestion>> fetchSuggestions(String query) async { Future<List<Suggestion>> fetchSuggestions(String query) async {
final Uri uri = Uri.parse( final Uri uri = Uri.parse('https://api.f0ck.me/search/?q=$query');
'https://f0ck.me/api/v2/admin/tags/suggest?q=$query', try {
); // wip: new route in pyapi final http.Response response = await http
final response = await http.get(uri); .get(uri)
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final Map<String, dynamic> decoded = jsonDecode(response.body); final dynamic decoded = jsonDecode(response.body);
if (decoded['success'] == true && decoded.containsKey('suggestions')) {
final List<dynamic> suggestionsList = decoded['suggestions']; if (decoded is List) {
return suggestionsList return decoded
.map( .map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
(dynamic jsonItem) =>
Suggestion.fromJson(jsonItem as Map<String, dynamic>),
)
.toList() .toList()
..sort( ..sort((a, b) => b.score.compareTo(a.score));
(Suggestion a, Suggestion b) =>
(b.score * b.tagged).compareTo(a.score * a.tagged),
);
} else { } else {
throw Exception('Nichts gefunden.'); throw Exception('Unerwartetes Format: Erwartet wurde eine Liste.');
} }
} else if (response.statusCode == 400) {
final dynamic error = jsonDecode(response.body);
final String message = error is Map<String, dynamic>
? error['detail']?.toString() ?? 'Unbekannter Fehler.'
: 'Unbekannter Fehler.';
throw Exception('Client-Fehler 400: $message');
} else { } else {
throw Exception( throw Exception(
'Fehler beim Abrufen der Vorschläge: ${response.statusCode}', 'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
); );
} }
} on TimeoutException {
throw Exception('Anfrage an die API hat zu lange gedauert.');
} catch (e) {
throw Exception('Fehler beim Verarbeiten der Anfrage: $e');
}
} }
Future<bool> login(String username, String password) async { Future<bool> login(String username, String password) async {
final Uri url = Uri.parse('https://api.f0ck.me/login'); final Uri url = Uri.parse('https://api.f0ck.me/login');
final response = await http.post( final http.Response response = await http.post(
url, url,
body: {'username': username, 'password': password}, body: {'username': username, 'password': password},
); );

View File

@ -1,20 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/services/api_service.dart'; import 'package:f0ckapp/services/api_service.dart';
import 'package:f0ckapp/models/suggestion_model.dart'; import 'package:f0ckapp/models/suggestion_model.dart';
import 'package:f0ckapp/providers/media_provider.dart'; import 'package:f0ckapp/providers/media_provider.dart';
class CustomSearchDelegate extends SearchDelegate<String> { class CustomSearchDelegate extends SearchDelegate<String> {
Timer? _debounceTimer;
List<Suggestion>? _suggestions;
bool _isLoading = false;
String? _error;
String _lastFetchedQuery = "";
@override @override
List<Widget> buildActions(BuildContext context) { List<Widget> buildActions(BuildContext context) {
return [ return [
IconButton( IconButton(
icon: Icon(Icons.clear), icon: const Icon(Icons.clear),
onPressed: () { onPressed: () {
query = ''; query = '';
_clearResults();
showSuggestions(context); showSuggestions(context);
}, },
), ),
@ -24,8 +29,11 @@ class CustomSearchDelegate extends SearchDelegate<String> {
@override @override
Widget buildLeading(BuildContext context) { Widget buildLeading(BuildContext context) {
return IconButton( return IconButton(
icon: Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => close(context, 'null'), onPressed: () {
_debounceTimer?.cancel();
close(context, 'null');
},
); );
} }
@ -36,35 +44,58 @@ class CustomSearchDelegate extends SearchDelegate<String> {
@override @override
Widget buildSuggestions(BuildContext context) { Widget buildSuggestions(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
if (query.isEmpty) { if (query.isEmpty) {
return Container(padding: EdgeInsets.all(16.0), child: Text('')); _debounceTimer?.cancel();
return Container(padding: const EdgeInsets.all(16.0), child: const Text(''));
} }
final Future<List<Suggestion>> futureSuggestions = Future.delayed( if (query != _lastFetchedQuery) {
Duration(milliseconds: 300), _debounceTimer?.cancel();
() => fetchSuggestions(query), _isLoading = true;
); _error = null;
_suggestions = null;
return FutureBuilder<List<Suggestion>>( _debounceTimer = Timer(Duration(milliseconds: 500), () async {
future: futureSuggestions, try {
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> snapshot) { final List<Suggestion> results = await fetchSuggestions(query);
if (snapshot.connectionState == ConnectionState.waiting) { _lastFetchedQuery = query;
return Center(child: CircularProgressIndicator()); setState(() {
_suggestions = results;
_isLoading = false;
});
} catch (e) {
_lastFetchedQuery = query;
setState(() {
_error = e.toString();
_suggestions = [];
_isLoading = false;
});
} }
if (snapshot.hasError) { });
return Center(child: Text("Fehler: ${snapshot.error}"));
} return Center(child: _buildLoadingIndicator());
if (!snapshot.hasData || snapshot.data!.isEmpty) { }
return Center(child: Text("Keine Vorschläge gefunden."));
if (_isLoading) {
return Center(child: _buildLoadingIndicator());
}
if (_error != null) {
return Center(child: Text("Fehler: $_error"));
}
if (_suggestions == null || _suggestions!.isEmpty) {
return Center(child: const Text("Keine Ergebnisse gefunden."));
} }
final List<Suggestion> suggestions = snapshot.data!;
return Consumer( return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) { builder: (BuildContext context, WidgetRef ref, Widget? child) {
return ListView.builder( return ListView.builder(
itemCount: suggestions.length, itemCount: _suggestions!.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final Suggestion suggestion = suggestions[index]; final Suggestion suggestion = _suggestions![index];
return ListTile( return ListTile(
title: Text(suggestion.tag), title: Text(suggestion.tag),
subtitle: Text( subtitle: Text(
@ -83,4 +114,32 @@ class CustomSearchDelegate extends SearchDelegate<String> {
}, },
); );
} }
Widget _buildLoadingIndicator() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(strokeWidth: 3.0),
const SizedBox(height: 12),
const Text(
'Vorschläge werden geladen...',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
);
}
void _clearResults() {
_debounceTimer?.cancel();
_suggestions = null;
_isLoading = false;
_error = null;
_lastFetchedQuery = "";
}
@override
void close(BuildContext context, String result) {
_debounceTimer?.cancel();
super.close(context, result);
}
} }

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.11+41 version: 1.1.12+42
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta