- search schmearch
This commit is contained in:
		@@ -3,12 +3,16 @@ class Suggestion {
 | 
			
		||||
  final int tagged;
 | 
			
		||||
  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) {
 | 
			
		||||
    return Suggestion(
 | 
			
		||||
      tag: json['tag'].toString(),
 | 
			
		||||
      tagged: int.tryParse(json['tagged'].toString()) ?? 0,
 | 
			
		||||
      tagged: json['tagged'],
 | 
			
		||||
      score: (json['score'] as num).toDouble(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,14 +4,12 @@ import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.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/utils/appversion_util.dart';
 | 
			
		||||
import 'package:f0ckapp/providers/theme_provider.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 {
 | 
			
		||||
  const MediaGrid({super.key});
 | 
			
		||||
 | 
			
		||||
@@ -56,8 +54,8 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
    final MediaState mediaState = ref.watch(mediaProvider);
 | 
			
		||||
    final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      key: _scaffoldKey,
 | 
			
		||||
@@ -80,14 +78,12 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: Icon(Icons.search),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              showSearch(
 | 
			
		||||
            icon: const Icon(Icons.search),
 | 
			
		||||
            onPressed: () async {
 | 
			
		||||
              await showSearch(
 | 
			
		||||
                context: context,
 | 
			
		||||
                delegate: CustomSearchDelegate(),
 | 
			
		||||
              );
 | 
			
		||||
              //mediaNotifier.setTag('drachenlord');
 | 
			
		||||
              //_scrollController.jumpTo(0);
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
@@ -163,7 +159,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
              ),
 | 
			
		||||
              child: null,
 | 
			
		||||
            ),
 | 
			
		||||
            ExpansionTile(
 | 
			
		||||
            /*ExpansionTile(
 | 
			
		||||
              title: const Text('Login'),
 | 
			
		||||
              children: [
 | 
			
		||||
                Padding(
 | 
			
		||||
@@ -193,7 +189,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
                            const SnackBar(
 | 
			
		||||
                              content: Text("noch nicht implementiert lol"),
 | 
			
		||||
                            ),
 | 
			
		||||
                            /*final success = await login(
 | 
			
		||||
                            final success = await login(
 | 
			
		||||
                              _usernameController.text,
 | 
			
		||||
                              _passwordController.text,
 | 
			
		||||
                            );
 | 
			
		||||
@@ -204,7 +200,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
                              ScaffoldMessenger.of(context).showSnackBar(
 | 
			
		||||
                                SnackBar(content: Text("Login fehlgeschlagen!")),
 | 
			
		||||
                              );
 | 
			
		||||
                            }*/
 | 
			
		||||
                            }
 | 
			
		||||
                          );
 | 
			
		||||
                        },
 | 
			
		||||
                        child: const Text('Login'),
 | 
			
		||||
@@ -213,7 +209,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            ),*/
 | 
			
		||||
            ExpansionTile(
 | 
			
		||||
              title: const Text('Theme'),
 | 
			
		||||
              children: [
 | 
			
		||||
@@ -221,10 +217,10 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    children: themeMap.entries.map((entry) {
 | 
			
		||||
                      final themeName = entry.key;
 | 
			
		||||
                      final themeData = entry.value;
 | 
			
		||||
                      final currentTheme = ref.watch(themeNotifierProvider);
 | 
			
		||||
                      final isSelected = currentTheme == themeData;
 | 
			
		||||
                      final String themeName = entry.key;
 | 
			
		||||
                      final ThemeData themeData = entry.value;
 | 
			
		||||
                      final ThemeData currentTheme = ref.watch(themeNotifierProvider);
 | 
			
		||||
                      final bool isSelected = currentTheme == themeData;
 | 
			
		||||
                      return ListTile(
 | 
			
		||||
                        title: Text(themeName),
 | 
			
		||||
                        selected: isSelected,
 | 
			
		||||
@@ -284,11 +280,11 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
 | 
			
		||||
          ),
 | 
			
		||||
          itemCount:
 | 
			
		||||
              mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0),
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
          itemBuilder: (BuildContext context, int index) {
 | 
			
		||||
            if (index >= mediaState.mediaItems.length) {
 | 
			
		||||
              return const Center(child: CircularProgressIndicator());
 | 
			
		||||
            }
 | 
			
		||||
            final item = mediaState.mediaItems[index];
 | 
			
		||||
            final MediaItem item = mediaState.mediaItems[index];
 | 
			
		||||
 | 
			
		||||
            return InkWell(
 | 
			
		||||
              onTap: () async {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
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/suggestion_model.dart';
 | 
			
		||||
 | 
			
		||||
final FlutterSecureStorage storage = FlutterSecureStorage();
 | 
			
		||||
final FlutterSecureStorage storage = const FlutterSecureStorage(
 | 
			
		||||
  aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
Future<List<MediaItem>> fetchMedia({
 | 
			
		||||
  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) {
 | 
			
		||||
    final List<dynamic> jsonList = jsonDecode(response.body);
 | 
			
		||||
    return jsonList.map((item) => MediaItem.fromJson(item)).toList();
 | 
			
		||||
@@ -37,7 +40,7 @@ Future<List<MediaItem>> fetchMedia({
 | 
			
		||||
Future<MediaItem> fetchMediaDetail(int itemId) async {
 | 
			
		||||
  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) {
 | 
			
		||||
    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 {
 | 
			
		||||
  final Uri uri = Uri.parse(
 | 
			
		||||
    'https://f0ck.me/api/v2/admin/tags/suggest?q=$query',
 | 
			
		||||
  ); // wip: new route in pyapi
 | 
			
		||||
  final response = await http.get(uri);
 | 
			
		||||
  final Uri uri = Uri.parse('https://api.f0ck.me/search/?q=$query');
 | 
			
		||||
  try {
 | 
			
		||||
    final http.Response response = await http
 | 
			
		||||
        .get(uri)
 | 
			
		||||
        .timeout(const Duration(seconds: 5));
 | 
			
		||||
 | 
			
		||||
    if (response.statusCode == 200) {
 | 
			
		||||
    final Map<String, dynamic> decoded = jsonDecode(response.body);
 | 
			
		||||
    if (decoded['success'] == true && decoded.containsKey('suggestions')) {
 | 
			
		||||
      final List<dynamic> suggestionsList = decoded['suggestions'];
 | 
			
		||||
      return suggestionsList
 | 
			
		||||
          .map(
 | 
			
		||||
            (dynamic jsonItem) =>
 | 
			
		||||
                Suggestion.fromJson(jsonItem as Map<String, dynamic>),
 | 
			
		||||
          )
 | 
			
		||||
      final dynamic decoded = jsonDecode(response.body);
 | 
			
		||||
 | 
			
		||||
      if (decoded is List) {
 | 
			
		||||
        return decoded
 | 
			
		||||
            .map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
 | 
			
		||||
            .toList()
 | 
			
		||||
        ..sort(
 | 
			
		||||
          (Suggestion a, Suggestion b) =>
 | 
			
		||||
              (b.score * b.tagged).compareTo(a.score * a.tagged),
 | 
			
		||||
        );
 | 
			
		||||
          ..sort((a, b) => b.score.compareTo(a.score));
 | 
			
		||||
      } 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 {
 | 
			
		||||
      throw Exception(
 | 
			
		||||
        '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 {
 | 
			
		||||
  final Uri url = Uri.parse('https://api.f0ck.me/login');
 | 
			
		||||
 | 
			
		||||
  final response = await http.post(
 | 
			
		||||
  final http.Response response = await http.post(
 | 
			
		||||
    url,
 | 
			
		||||
    body: {'username': username, 'password': password},
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,25 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/services/api_service.dart';
 | 
			
		||||
import 'package:f0ckapp/models/suggestion_model.dart';
 | 
			
		||||
import 'package:f0ckapp/providers/media_provider.dart';
 | 
			
		||||
 | 
			
		||||
class CustomSearchDelegate extends SearchDelegate<String> {
 | 
			
		||||
  Timer? _debounceTimer;
 | 
			
		||||
  List<Suggestion>? _suggestions;
 | 
			
		||||
  bool _isLoading = false;
 | 
			
		||||
  String? _error;
 | 
			
		||||
  String _lastFetchedQuery = "";
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  List<Widget> buildActions(BuildContext context) {
 | 
			
		||||
    return [
 | 
			
		||||
      IconButton(
 | 
			
		||||
        icon: Icon(Icons.clear),
 | 
			
		||||
        icon: const Icon(Icons.clear),
 | 
			
		||||
        onPressed: () {
 | 
			
		||||
          query = '';
 | 
			
		||||
          _clearResults();
 | 
			
		||||
          showSuggestions(context);
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
@@ -24,8 +29,11 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildLeading(BuildContext context) {
 | 
			
		||||
    return IconButton(
 | 
			
		||||
      icon: Icon(Icons.arrow_back),
 | 
			
		||||
      onPressed: () => close(context, 'null'),
 | 
			
		||||
      icon: const Icon(Icons.arrow_back),
 | 
			
		||||
      onPressed: () {
 | 
			
		||||
        _debounceTimer?.cancel();
 | 
			
		||||
        close(context, 'null');
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -36,35 +44,58 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget buildSuggestions(BuildContext context) {
 | 
			
		||||
    return StatefulBuilder(
 | 
			
		||||
      builder: (BuildContext context, void Function(void Function()) setState) {
 | 
			
		||||
        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(
 | 
			
		||||
      Duration(milliseconds: 300),
 | 
			
		||||
      () => fetchSuggestions(query),
 | 
			
		||||
    );
 | 
			
		||||
        if (query != _lastFetchedQuery) {
 | 
			
		||||
          _debounceTimer?.cancel();
 | 
			
		||||
          _isLoading = true;
 | 
			
		||||
          _error = null;
 | 
			
		||||
          _suggestions = null;
 | 
			
		||||
 | 
			
		||||
    return FutureBuilder<List<Suggestion>>(
 | 
			
		||||
      future: futureSuggestions,
 | 
			
		||||
      builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> snapshot) {
 | 
			
		||||
        if (snapshot.connectionState == ConnectionState.waiting) {
 | 
			
		||||
          return Center(child: CircularProgressIndicator());
 | 
			
		||||
          _debounceTimer = Timer(Duration(milliseconds: 500), () async {
 | 
			
		||||
            try {
 | 
			
		||||
              final List<Suggestion> results = await fetchSuggestions(query);
 | 
			
		||||
              _lastFetchedQuery = query;
 | 
			
		||||
              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}"));
 | 
			
		||||
        }
 | 
			
		||||
        if (!snapshot.hasData || snapshot.data!.isEmpty) {
 | 
			
		||||
          return Center(child: Text("Keine Vorschläge gefunden."));
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          return Center(child: _buildLoadingIndicator());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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(
 | 
			
		||||
          builder: (BuildContext context, WidgetRef ref, Widget? child) {
 | 
			
		||||
            return ListView.builder(
 | 
			
		||||
              itemCount: suggestions.length,
 | 
			
		||||
              itemCount: _suggestions!.length,
 | 
			
		||||
              itemBuilder: (BuildContext context, int index) {
 | 
			
		||||
                final Suggestion suggestion = suggestions[index];
 | 
			
		||||
                final Suggestion suggestion = _suggestions![index];
 | 
			
		||||
                return ListTile(
 | 
			
		||||
                  title: Text(suggestion.tag),
 | 
			
		||||
                  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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
# 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.
 | 
			
		||||
version: 1.1.11+41
 | 
			
		||||
version: 1.1.12+42
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.9.0-100.2.beta
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user