diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index cb09871..21a7961 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,7 +30,7 @@
-
+
diff --git a/lib/main.dart b/lib/main.dart
index 54aa3af..26dd4fd 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -28,20 +28,20 @@ class F0ckApp extends ConsumerWidget {
routes: [
GoRoute(
path: '/',
- builder: (context, state) {
+ builder: (BuildContext context, GoRouterState state) {
return const MediaGrid();
},
),
GoRoute(
path: '/:rest(.*)',
builder: (context, state) {
- final isInternalLink = (state.extra is bool && state.extra == true);
- final fullPath = state.matchedLocation;
+ final bool isInternalLink = (state.extra is bool && state.extra == true);
+ final String fullPath = state.matchedLocation;
final regExp = RegExp(
r'^(?:/tag/(?.+?))?(?:/(?image|audio|video))?(?:/(?\d+))?$',
);
- final match = regExp.firstMatch(fullPath);
+ final RegExpMatch? match = regExp.firstMatch(fullPath);
if (match == null) {
return const Scaffold(body: Center(child: Text('Ungültiger Link')));
@@ -51,12 +51,12 @@ class F0ckApp extends ConsumerWidget {
final String? mime = match.namedGroup('mime');
final String? idStr = match.namedGroup('itemid');
final int? itemId = idStr != null ? int.tryParse(idStr) : null;
- const preloadOffset = 50;
+ const int preloadOffset = 50;
return Consumer(
- builder: (context, ref, child) {
+ builder: (BuildContext context, WidgetRef ref, Widget? child) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
- final mediaNotifier = ref.read(mediaProvider.notifier);
+ final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
if (!isInternalLink) {
mediaNotifier.setType(mime ?? "alles");
mediaNotifier.setTag(tag);
@@ -79,7 +79,7 @@ class F0ckApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- final theme = ref.watch(themeNotifierProvider);
+ final ThemeData theme = ref.watch(themeNotifierProvider);
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: _router,
diff --git a/lib/models/suggestion_model.dart b/lib/models/suggestion_model.dart
new file mode 100644
index 0000000..8949f70
--- /dev/null
+++ b/lib/models/suggestion_model.dart
@@ -0,0 +1,15 @@
+class Suggestion {
+ final String tag;
+ final int tagged;
+ final double score;
+
+ Suggestion({required this.tag, required this.tagged, required this.score});
+
+ factory Suggestion.fromJson(Map json) {
+ return Suggestion(
+ tag: json['tag'].toString(),
+ tagged: int.tryParse(json['tagged'].toString()) ?? 0,
+ score: (json['score'] as num).toDouble(),
+ );
+ }
+}
diff --git a/lib/providers/media_provider.dart b/lib/providers/media_provider.dart
index ad098bc..4dc0242 100644
--- a/lib/providers/media_provider.dart
+++ b/lib/providers/media_provider.dart
@@ -102,8 +102,17 @@ class MediaNotifier extends StateNotifier {
}
void addMediaItems(List newItems) {
- final updated = List.from(state.mediaItems)..addAll(newItems);
- state = state.replace(mediaItems: updated);
+ final Set existingIds = state.mediaItems
+ .map((item) => item.id)
+ .toSet();
+ final List filteredItems = newItems
+ .where((item) => !existingIds.contains(item.id))
+ .toList();
+ if (filteredItems.isNotEmpty) {
+ final List updated = List.from(state.mediaItems)
+ ..addAll(filteredItems);
+ state = state.replace(mediaItems: updated);
+ }
}
Future loadMedia({int? id}) async {
diff --git a/lib/screens/detailview_screen.dart b/lib/screens/detailview_screen.dart
index 008099a..5e819ff 100644
--- a/lib/screens/detailview_screen.dart
+++ b/lib/screens/detailview_screen.dart
@@ -56,13 +56,13 @@ class _DetailViewState extends ConsumerState {
try {
await ref.read(mediaProvider.notifier).loadMedia();
} catch (e) {
- _showError("Fehler beim Laden der Medien: $e");
+ _showMsg("Fehler beim Laden der Medien: $e");
} finally {
setState(() => isLoading = false);
}
}
- void _showError(String message) {
+ void _showMsg(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
@@ -70,15 +70,15 @@ class _DetailViewState extends ConsumerState {
}
Future _downloadMedia() async {
- final mediaState = ref.read(mediaProvider);
- final currentItem = mediaState.mediaItems[_currentIndex];
+ final MediaState mediaState = ref.read(mediaProvider);
+ final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
if (Platform.isAndroid || Platform.isIOS) {
- var status = await Permission.storage.status;
+ PermissionStatus status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
if (!status.isGranted) {
- _showError("Speicherberechtigung wurde nicht erteilt.");
+ _showMsg("Speicherberechtigung wurde nicht erteilt.");
return;
}
}
@@ -86,17 +86,17 @@ class _DetailViewState extends ConsumerState {
String localPath;
if (Platform.isAndroid) {
- final directory = await getExternalStorageDirectory();
+ final Directory? directory = await getExternalStorageDirectory();
localPath = "${directory!.path}/Download/fApp";
} else if (Platform.isIOS) {
- final directory = await getApplicationDocumentsDirectory();
+ final Directory directory = await getApplicationDocumentsDirectory();
localPath = directory.path;
} else {
- final directory = await getTemporaryDirectory();
+ final Directory directory = await getTemporaryDirectory();
localPath = directory.path;
}
- final savedDir = Directory(localPath);
+ final Directory savedDir = Directory(localPath);
if (!await savedDir.exists()) {
await savedDir.create(recursive: true);
}
@@ -111,10 +111,10 @@ class _DetailViewState extends ConsumerState {
);
if (mounted) {
- _showError('Download gestartet: ${currentItem.mediaUrl}');
+ _showMsg('Download gestartet: ${currentItem.mediaUrl}');
}
} catch (e) {
- _showError('Download fehlgeschlagen: $e');
+ _showMsg('Download fehlgeschlagen: $e');
}
}
@@ -126,11 +126,10 @@ class _DetailViewState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final mediaState = ref.watch(mediaProvider);
+ final MediaState mediaState = ref.watch(mediaProvider);
final int itemIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
- print('itemIndex: ${itemIndex}; initial: ${widget.initialItemId}');
if (itemIndex == -1) {
Future.microtask(() {
@@ -168,7 +167,7 @@ class _DetailViewState extends ConsumerState {
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
- _showError('download ist wip');
+ _showMsg('fullscreen ist wip');
},
),
IconButton(
@@ -261,7 +260,7 @@ class _DetailViewState extends ConsumerState {
}
Widget _buildMediaItem(MediaItem item, bool isActive) {
- final mediaNotifier = ref.read(mediaProvider.notifier);
+ final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return SingleChildScrollView(
child: Column(
diff --git a/lib/screens/mediagrid_screen.dart b/lib/screens/mediagrid_screen.dart
index af3aa31..feabe47 100644
--- a/lib/screens/mediagrid_screen.dart
+++ b/lib/screens/mediagrid_screen.dart
@@ -7,6 +7,7 @@ import 'package:go_router/go_router.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 mediaTypes = ["alles", "image", "video", "audio"];
const List mediaModes = ["sfw", "nsfw", "untagged", "all"];
@@ -78,6 +79,17 @@ class _MediaGridState extends ConsumerState {
},
),
actions: [
+ IconButton(
+ icon: Icon(Icons.search),
+ onPressed: () {
+ showSearch(
+ context: context,
+ delegate: CustomSearchDelegate(),
+ );
+ //mediaNotifier.setTag('drachenlord');
+ //_scrollController.jumpTo(0);
+ },
+ ),
IconButton(
icon: Icon(
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart
index c5dbeb2..3ab755a 100644
--- a/lib/services/api_service.dart
+++ b/lib/services/api_service.dart
@@ -4,8 +4,9 @@ import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
+import 'package:f0ckapp/models/suggestion_model.dart';
-final storage = FlutterSecureStorage();
+final FlutterSecureStorage storage = FlutterSecureStorage();
Future> fetchMedia({
int? older,
@@ -48,6 +49,36 @@ Future fetchMediaDetail(int itemId) async {
}
}
+Future> 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);
+
+ if (response.statusCode == 200) {
+ final Map decoded = jsonDecode(response.body);
+ if (decoded['success'] == true && decoded.containsKey('suggestions')) {
+ final List suggestionsList = decoded['suggestions'];
+ return suggestionsList
+ .map(
+ (dynamic jsonItem) =>
+ Suggestion.fromJson(jsonItem as Map),
+ )
+ .toList()
+ ..sort(
+ (Suggestion a, Suggestion b) =>
+ (b.score * b.tagged).compareTo(a.score * a.tagged),
+ );
+ } else {
+ throw Exception('Nichts gefunden.');
+ }
+ } else {
+ throw Exception(
+ 'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
+ );
+ }
+}
+
Future login(String username, String password) async {
final Uri url = Uri.parse('https://api.f0ck.me/login');
@@ -57,8 +88,8 @@ Future login(String username, String password) async {
);
if (response.statusCode == 200) {
- final data = jsonDecode(response.body);
- final token = data['token'];
+ final dynamic data = jsonDecode(response.body);
+ final dynamic token = data['token'];
await storage.write(key: "token", value: token);
diff --git a/lib/utils/customsearchdelegate_util.dart b/lib/utils/customsearchdelegate_util.dart
new file mode 100644
index 0000000..f661837
--- /dev/null
+++ b/lib/utils/customsearchdelegate_util.dart
@@ -0,0 +1,86 @@
+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 {
+ @override
+ List buildActions(BuildContext context) {
+ return [
+ IconButton(
+ icon: Icon(Icons.clear),
+ onPressed: () {
+ query = '';
+ showSuggestions(context);
+ },
+ ),
+ ];
+ }
+
+ @override
+ Widget buildLeading(BuildContext context) {
+ return IconButton(
+ icon: Icon(Icons.arrow_back),
+ onPressed: () => close(context, 'null'),
+ );
+ }
+
+ @override
+ Widget buildResults(BuildContext context) {
+ return Center(child: Text('Suchergebnisse für: "$query"'));
+ }
+
+ @override
+ Widget buildSuggestions(BuildContext context) {
+ if (query.isEmpty) {
+ return Container(padding: EdgeInsets.all(16.0), child: Text(''));
+ }
+
+ final Future> futureSuggestions = Future.delayed(
+ Duration(milliseconds: 300),
+ () => fetchSuggestions(query),
+ );
+
+ return FutureBuilder>(
+ future: futureSuggestions,
+ builder: (BuildContext context, AsyncSnapshot> snapshot) {
+ if (snapshot.connectionState == ConnectionState.waiting) {
+ return Center(child: CircularProgressIndicator());
+ }
+ if (snapshot.hasError) {
+ return Center(child: Text("Fehler: ${snapshot.error}"));
+ }
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return Center(child: Text("Keine Vorschläge gefunden."));
+ }
+
+ final List suggestions = snapshot.data!;
+ return Consumer(
+ builder: (BuildContext context, WidgetRef ref, Widget? child) {
+ return ListView.builder(
+ itemCount: suggestions.length,
+ itemBuilder: (BuildContext context, int index) {
+ final Suggestion suggestion = suggestions[index];
+ return ListTile(
+ title: Text(suggestion.tag),
+ subtitle: Text(
+ 'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
+ style: TextStyle(fontSize: 12),
+ ),
+ onTap: () {
+ ref.read(mediaProvider.notifier).setTag(suggestion.tag);
+ close(context, suggestion.tag);
+ },
+ );
+ },
+ );
+ },
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/video_widget.dart b/lib/widgets/video_widget.dart
index 61debb4..cac9383 100644
--- a/lib/widgets/video_widget.dart
+++ b/lib/widgets/video_widget.dart
@@ -45,7 +45,7 @@ class _VideoWidgetState extends ConsumerState {
}
_controller.setLooping(true);
- final muted = ref.read(mediaProvider).muted;
+ final bool muted = ref.read(mediaProvider).muted;
_controller.setVolume(muted ? 0.0 : 1.0);
}
@@ -82,7 +82,7 @@ class _VideoWidgetState extends ConsumerState {
@override
Widget build(BuildContext context) {
- final muted = ref.watch(mediaProvider).muted;
+ final bool muted = ref.watch(mediaProvider).muted;
if (_controller.value.isInitialized &&
_controller.value.volume != (muted ? 0.0 : 1.0)) {
_controller.setVolume(muted ? 0.0 : 1.0);
diff --git a/lib/widgets/videooverlay_widget.dart b/lib/widgets/videooverlay_widget.dart
index 07b6a2b..d9beaf8 100644
--- a/lib/widgets/videooverlay_widget.dart
+++ b/lib/widgets/videooverlay_widget.dart
@@ -17,8 +17,8 @@ class VideoControlsOverlay extends ConsumerWidget {
@override
Widget build(BuildContext context, ref) {
- 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 Stack(
alignment: Alignment.center,
diff --git a/pubspec.yaml b/pubspec.yaml
index d7106e1..756cc84 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -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.10+40
+version: 1.1.11+41
environment:
sdk: ^3.9.0-100.2.beta