Compare commits

...

4 Commits

Author SHA1 Message Date
f75299f0d4 xd
All checks were successful
Flutter Schmutter / build (push) Successful in 3m53s
2025-06-10 18:53:52 +02:00
03c6431eca v1.1.13+43
- fk android
2025-06-10 18:53:07 +02:00
5876c809a5 v1.1.12+42
All checks were successful
Flutter Schmutter / build (push) Successful in 3m45s
- search schmearch
2025-06-10 11:07:00 +02:00
c35308fbc1 v1.1.11+41
All checks were successful
Flutter Schmutter / build (push) Successful in 3m57s
- fixed: duplicates on the frontpage
- new: search by tag
2025-06-10 08:39:55 +02:00
13 changed files with 313 additions and 155 deletions

View File

@ -51,3 +51,6 @@ jobs:
files: |- files: |-
build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/app-release.apk
token: '${{secrets.RELEASE_TOKEN}}' token: '${{secrets.RELEASE_TOKEN}}'
- name: trigger fdroid puller
run: curl https://flumm.io/pullfdroid.php?token=${{secrets.PULLER_TOKEN}}

View File

@ -1,8 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application <application
android:label="f0ckapp" android:label="f0ckapp"
android:name="${applicationName}" android:name="${applicationName}"
@ -16,8 +15,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize">
android:requestLegacyExternalStorage="true">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
@ -30,7 +28,7 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="true"/> <meta-data android:name="flutter_deeplinking_enabled" android:value="false"/>
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>

View File

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:f0ckapp/screens/mediagrid_screen.dart'; import 'package:f0ckapp/screens/mediagrid_screen.dart';
import 'package:f0ckapp/screens/detailview_screen.dart'; import 'package:f0ckapp/screens/detailview_screen.dart';
@ -14,7 +13,6 @@ import 'package:f0ckapp/providers/theme_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await FlutterDownloader.initialize();
await AppVersion.init(); await AppVersion.init();
runApp(ProviderScope(child: F0ckApp())); runApp(ProviderScope(child: F0ckApp()));
@ -28,20 +26,20 @@ class F0ckApp extends ConsumerWidget {
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/',
builder: (context, state) { builder: (BuildContext context, GoRouterState state) {
return const MediaGrid(); return const MediaGrid();
}, },
), ),
GoRoute( GoRoute(
path: '/:rest(.*)', path: '/:rest(.*)',
builder: (context, state) { builder: (context, state) {
final isInternalLink = (state.extra is bool && state.extra == true); final bool isInternalLink = (state.extra is bool && state.extra == true);
final fullPath = state.matchedLocation; final String fullPath = state.matchedLocation;
final regExp = RegExp( final regExp = RegExp(
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$', r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
); );
final match = regExp.firstMatch(fullPath); final RegExpMatch? match = regExp.firstMatch(fullPath);
if (match == null) { if (match == null) {
return const Scaffold(body: Center(child: Text('Ungültiger Link'))); return const Scaffold(body: Center(child: Text('Ungültiger Link')));
@ -51,12 +49,12 @@ class F0ckApp extends ConsumerWidget {
final String? mime = match.namedGroup('mime'); final String? mime = match.namedGroup('mime');
final String? idStr = match.namedGroup('itemid'); final String? idStr = match.namedGroup('itemid');
final int? itemId = idStr != null ? int.tryParse(idStr) : null; final int? itemId = idStr != null ? int.tryParse(idStr) : null;
const preloadOffset = 50; const int preloadOffset = 50;
return Consumer( return Consumer(
builder: (context, ref, child) { builder: (BuildContext context, WidgetRef ref, Widget? child) {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
final mediaNotifier = ref.read(mediaProvider.notifier); final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
if (!isInternalLink) { if (!isInternalLink) {
mediaNotifier.setType(mime ?? "alles"); mediaNotifier.setType(mime ?? "alles");
mediaNotifier.setTag(tag); mediaNotifier.setTag(tag);
@ -79,7 +77,7 @@ class F0ckApp extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themeNotifierProvider); final ThemeData theme = ref.watch(themeNotifierProvider);
return MaterialApp.router( return MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
routerConfig: _router, routerConfig: _router,

View File

@ -0,0 +1,19 @@
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<String, dynamic> json) {
return Suggestion(
tag: json['tag'].toString(),
tagged: json['tagged'],
score: (json['score'] as num).toDouble(),
);
}
}

View File

@ -102,8 +102,17 @@ class MediaNotifier extends StateNotifier<MediaState> {
} }
void addMediaItems(List<MediaItem> newItems) { void addMediaItems(List<MediaItem> newItems) {
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems); final Set<int> existingIds = state.mediaItems
state = state.replace(mediaItems: updated); .map((item) => item.id)
.toSet();
final List<MediaItem> filteredItems = newItems
.where((item) => !existingIds.contains(item.id))
.toList();
if (filteredItems.isNotEmpty) {
final List<MediaItem> updated = List<MediaItem>.from(state.mediaItems)
..addAll(filteredItems);
state = state.replace(mediaItems: updated);
}
} }
Future<void> loadMedia({int? id}) async { Future<void> loadMedia({int? id}) async {

View File

@ -6,10 +6,8 @@ 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:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:downloadsfolder/downloadsfolder.dart' as blah;
import 'package:f0ckapp/models/mediaitem_model.dart'; import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/widgets/video_widget.dart'; import 'package:f0ckapp/widgets/video_widget.dart';
@ -56,13 +54,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
try { try {
await ref.read(mediaProvider.notifier).loadMedia(); await ref.read(mediaProvider.notifier).loadMedia();
} catch (e) { } catch (e) {
_showError("Fehler beim Laden der Medien: $e"); _showMsg("Fehler beim Laden der Medien: $e");
} finally { } finally {
setState(() => isLoading = false); setState(() => isLoading = false);
} }
} }
void _showError(String message) { void _showMsg(String message) {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
..removeCurrentSnackBar() ..removeCurrentSnackBar()
@ -70,51 +68,18 @@ class _DetailViewState extends ConsumerState<DetailView> {
} }
Future<void> _downloadMedia() async { Future<void> _downloadMedia() async {
final mediaState = ref.read(mediaProvider); final MediaState mediaState = ref.read(mediaProvider);
final currentItem = mediaState.mediaItems[_currentIndex]; final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
final file = await DefaultCacheManager().getSingleFile(currentItem.mediaUrl);
if (Platform.isAndroid || Platform.isIOS) { bool? success = await blah.copyFileIntoDownloadFolder(
var status = await Permission.storage.status; '${file.dirname}/${file.basename}',
if (!status.isGranted) { currentItem.mediaUrl.split('/').last
status = await Permission.storage.request(); );
if (!status.isGranted) { if (success == true) {
_showError("Speicherberechtigung wurde nicht erteilt."); _showMsg('${file.basename} wurde irgendwie heruntergeladen. Viel Spaß bei der Suche');
return;
}
}
}
String localPath;
if (Platform.isAndroid) {
final directory = await getExternalStorageDirectory();
localPath = "${directory!.path}/Download/fApp";
} else if (Platform.isIOS) {
final directory = await getApplicationDocumentsDirectory();
localPath = directory.path;
} else { } else {
final directory = await getTemporaryDirectory(); _showMsg('${file.basename} konnte nicht heruntergeladen werden.');
localPath = directory.path;
}
final savedDir = Directory(localPath);
if (!await savedDir.exists()) {
await savedDir.create(recursive: true);
}
try {
await FlutterDownloader.enqueue(
url: currentItem.mediaUrl,
savedDir: localPath,
fileName: currentItem.mediaUrl.split('/').last,
showNotification: true,
openFileFromNotification: true,
);
if (mounted) {
_showError('Download gestartet: ${currentItem.mediaUrl}');
}
} catch (e) {
_showError('Download fehlgeschlagen: $e');
} }
} }
@ -126,11 +91,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider); final MediaState mediaState = ref.watch(mediaProvider);
final int itemIndex = mediaState.mediaItems.indexWhere( final int itemIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId, (item) => item.id == widget.initialItemId,
); );
print('itemIndex: ${itemIndex}; initial: ${widget.initialItemId}');
if (itemIndex == -1) { if (itemIndex == -1) {
Future.microtask(() { Future.microtask(() {
@ -168,7 +132,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
IconButton( IconButton(
icon: const Icon(Icons.fullscreen), icon: const Icon(Icons.fullscreen),
onPressed: () { onPressed: () {
_showError('download ist wip'); _showMsg('fullscreen ist wip');
}, },
), ),
IconButton( IconButton(
@ -261,7 +225,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
} }
Widget _buildMediaItem(MediaItem item, bool isActive) { Widget _buildMediaItem(MediaItem item, bool isActive) {
final mediaNotifier = ref.read(mediaProvider.notifier); final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(

View File

@ -4,12 +4,11 @@ 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';
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});
@ -55,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,
@ -78,6 +77,15 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
}, },
), ),
actions: [ actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
await showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
},
),
IconButton( IconButton(
icon: Icon( icon: Icon(
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle, mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
@ -151,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(
@ -181,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,
); );
@ -192,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'),
@ -201,7 +209,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
), ),
], ],
), ),*/
ExpansionTile( ExpansionTile(
title: const Text('Theme'), title: const Text('Theme'),
children: [ children: [
@ -209,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,
@ -272,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,11 +1,15 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 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';
final storage = FlutterSecureStorage(); final FlutterSecureStorage storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
Future<List<MediaItem>> fetchMedia({ Future<List<MediaItem>> fetchMedia({
int? older, int? older,
@ -24,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();
@ -36,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);
@ -48,17 +52,53 @@ Future<MediaItem> fetchMediaDetail(int itemId) async {
} }
} }
Future<List<Suggestion>> fetchSuggestions(String query) async {
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 dynamic decoded = jsonDecode(response.body);
if (decoded is List) {
return decoded
.map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
.toList()
..sort((a, b) => b.score.compareTo(a.score));
} else {
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 { 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},
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = jsonDecode(response.body); final dynamic data = jsonDecode(response.body);
final token = data['token']; final dynamic token = data['token'];
await storage.write(key: "token", value: token); await storage.write(key: "token", value: token);

View File

@ -0,0 +1,145 @@
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: const Icon(Icons.clear),
onPressed: () {
query = '';
_clearResults();
showSuggestions(context);
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
_debounceTimer?.cancel();
close(context, 'null');
},
);
}
@override
Widget buildResults(BuildContext context) {
return Center(child: Text('Suchergebnisse für: "$query"'));
}
@override
Widget buildSuggestions(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
if (query.isEmpty) {
_debounceTimer?.cancel();
return Container(padding: const EdgeInsets.all(16.0), child: const Text(''));
}
if (query != _lastFetchedQuery) {
_debounceTimer?.cancel();
_isLoading = true;
_error = null;
_suggestions = null;
_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;
});
}
});
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."));
}
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);
},
);
},
);
},
);
},
);
}
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

@ -45,7 +45,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
} }
_controller.setLooping(true); _controller.setLooping(true);
final muted = ref.read(mediaProvider).muted; final bool muted = ref.read(mediaProvider).muted;
_controller.setVolume(muted ? 0.0 : 1.0); _controller.setVolume(muted ? 0.0 : 1.0);
} }
@ -82,7 +82,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final muted = ref.watch(mediaProvider).muted; final bool muted = ref.watch(mediaProvider).muted;
if (_controller.value.isInitialized && if (_controller.value.isInitialized &&
_controller.value.volume != (muted ? 0.0 : 1.0)) { _controller.value.volume != (muted ? 0.0 : 1.0)) {
_controller.setVolume(muted ? 0.0 : 1.0); _controller.setVolume(muted ? 0.0 : 1.0);

View File

@ -17,8 +17,8 @@ class VideoControlsOverlay extends ConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
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 Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -105,6 +105,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dartx:
dependency: transitive
description:
name: dartx
sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
diacritic:
dependency: transitive
description:
name: diacritic
sha256: "12981945ec38931748836cd76f2b38773118d0baef3c68404bdfde9566147876"
url: "https://pub.dev"
source: hosted
version: "0.1.6"
downloadsfolder:
dependency: "direct main"
description:
name: downloadsfolder
sha256: "0e1bb7dd634d6231c0ac116c467da94507a07ed62239712ea0dead981d58b114"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -150,14 +174,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
flutter_downloader:
dependency: "direct main"
description:
name: flutter_downloader
sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -393,7 +409,7 @@ packages:
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
dependency: "direct main" dependency: transitive
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@ -440,54 +456,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
url: "https://pub.dev"
source: hosted
version: "12.0.0+1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -653,6 +621,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.6"
time:
dependency: transitive
description:
name: time
sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

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.10+40 version: 1.1.13+43
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta
@ -42,9 +42,7 @@ dependencies:
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
go_router: ^15.1.3 go_router: ^15.1.3
flutter_downloader: ^1.12.0 downloadsfolder: ^1.2.0
permission_handler: ^12.0.0+1
path_provider: ^2.1.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: