Compare commits
7 Commits
v1.1.10+40
...
v1.1.15+45
Author | SHA1 | Date | |
---|---|---|---|
493422e724 | |||
3b95d128e1 | |||
57636c5de6 | |||
f75299f0d4 | |||
03c6431eca | |||
5876c809a5 | |||
c35308fbc1 |
@ -51,3 +51,9 @@ jobs:
|
||||
files: |-
|
||||
build/app/outputs/flutter-apk/app-release.apk
|
||||
token: '${{secrets.RELEASE_TOKEN}}'
|
||||
|
||||
- name: wait for gitea to catch up xD
|
||||
run: sleep 5
|
||||
|
||||
- name: trigger fdroid puller
|
||||
run: curl https://flumm.io/pullfdroid.php?token=${{secrets.PULLER_TOKEN}}
|
||||
|
@ -1,8 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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"/>
|
||||
|
||||
<application
|
||||
android:label="f0ckapp"
|
||||
android:name="${applicationName}"
|
||||
@ -16,8 +15,7 @@
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@ -30,7 +28,7 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</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">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
|
@ -1,5 +1,69 @@
|
||||
package com.f0ck.f0ckapp
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.NonNull
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val CHANNEL = "MediaShit"
|
||||
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine): Unit {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
|
||||
call,
|
||||
result ->
|
||||
if (call.method == "saveFile") {
|
||||
val filePath = call.argument<String>("filePath")
|
||||
val fileName = call.argument<String>("fileName")
|
||||
val subDir = call.argument<String?>("subDir")
|
||||
|
||||
if (filePath == null || fileName == null)
|
||||
result.error("SAVE_FAILED", "file not found", null)
|
||||
|
||||
if (!saveFileUsingMediaStore(applicationContext, filePath!!, fileName!!, subDir))
|
||||
result.error("COPY_FAILED", "Datei konnte nicht gespeichert werden", null)
|
||||
result.success(true)
|
||||
} else result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFileUsingMediaStore(
|
||||
context: Context,
|
||||
filePath: String,
|
||||
fileName: String,
|
||||
subDir: String?
|
||||
|
||||
): Boolean {
|
||||
val srcFile = File(filePath)
|
||||
if (!srcFile.exists()) return false
|
||||
|
||||
val values =
|
||||
ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + (subDir ?: "f0ck"))
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
val resolver = context.contentResolver
|
||||
val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
val uri = resolver.insert(collection, values) ?: return false
|
||||
|
||||
resolver.openOutputStream(uri).use { out ->
|
||||
FileInputStream(srcFile).use { input -> input.copyTo(out!!, 4096) }
|
||||
}
|
||||
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:go_router/go_router.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/detailview_screen.dart';
|
||||
@ -14,7 +13,6 @@ import 'package:f0ckapp/providers/theme_provider.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
await FlutterDownloader.initialize();
|
||||
await AppVersion.init();
|
||||
|
||||
runApp(ProviderScope(child: F0ckApp()));
|
||||
@ -28,20 +26,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/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\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 +49,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 +77,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,
|
||||
|
19
lib/models/suggestion_model.dart
Normal file
19
lib/models/suggestion_model.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -102,8 +102,17 @@ class MediaNotifier extends StateNotifier<MediaState> {
|
||||
}
|
||||
|
||||
void addMediaItems(List<MediaItem> newItems) {
|
||||
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
|
||||
state = state.replace(mediaItems: updated);
|
||||
final Set<int> existingIds = state.mediaItems
|
||||
.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 {
|
||||
|
@ -2,13 +2,12 @@ import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.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:f0ckapp/models/mediaitem_model.dart';
|
||||
@ -56,13 +55,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
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,52 +69,19 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
}
|
||||
|
||||
Future<void> _downloadMedia() async {
|
||||
final mediaState = ref.read(mediaProvider);
|
||||
final currentItem = mediaState.mediaItems[_currentIndex];
|
||||
final MediaState mediaState = ref.read(mediaProvider);
|
||||
final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
|
||||
final File file = await DefaultCacheManager().getSingleFile(currentItem.mediaUrl);
|
||||
final MethodChannel methodChannel = const MethodChannel('MediaShit');
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
var status = await Permission.storage.status;
|
||||
if (!status.isGranted) {
|
||||
status = await Permission.storage.request();
|
||||
if (!status.isGranted) {
|
||||
_showError("Speicherberechtigung wurde nicht erteilt.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
|
||||
'filePath': file.path,
|
||||
'fileName': currentItem.dest,
|
||||
});
|
||||
|
||||
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 {
|
||||
final directory = await getTemporaryDirectory();
|
||||
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');
|
||||
}
|
||||
success == true
|
||||
? _showMsg('${currentItem.dest} wurde in Downloads/fApp neigespeichert.')
|
||||
: _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.');
|
||||
}
|
||||
|
||||
@override
|
||||
@ -126,11 +92,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
|
||||
@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 +133,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
_showError('download ist wip');
|
||||
_showMsg('fullscreen ist wip');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
@ -261,7 +226,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
}
|
||||
|
||||
Widget _buildMediaItem(MediaItem item, bool isActive) {
|
||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
||||
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
|
@ -4,12 +4,11 @@ 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';
|
||||
|
||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||
import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
|
||||
|
||||
class MediaGrid extends ConsumerStatefulWidget {
|
||||
const MediaGrid({super.key});
|
||||
@ -55,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,
|
||||
@ -78,6 +77,15 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () async {
|
||||
await showSearch(
|
||||
context: context,
|
||||
delegate: CustomSearchDelegate(),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
|
||||
@ -151,7 +159,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
),
|
||||
child: null,
|
||||
),
|
||||
ExpansionTile(
|
||||
/*ExpansionTile(
|
||||
title: const Text('Login'),
|
||||
children: [
|
||||
Padding(
|
||||
@ -181,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,
|
||||
);
|
||||
@ -192,7 +200,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Login fehlgeschlagen!")),
|
||||
);
|
||||
}*/
|
||||
}
|
||||
);
|
||||
},
|
||||
child: const Text('Login'),
|
||||
@ -201,7 +209,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),*/
|
||||
ExpansionTile(
|
||||
title: const Text('Theme'),
|
||||
children: [
|
||||
@ -209,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,
|
||||
@ -272,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,11 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
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 = const FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
Future<List<MediaItem>> fetchMedia({
|
||||
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) {
|
||||
final List<dynamic> jsonList = jsonDecode(response.body);
|
||||
return jsonList.map((item) => MediaItem.fromJson(item)).toList();
|
||||
@ -36,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);
|
||||
|
||||
@ -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 {
|
||||
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},
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
|
145
lib/utils/customsearchdelegate_util.dart
Normal file
145
lib/utils/customsearchdelegate_util.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
|
||||
}
|
||||
_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<VideoWidget> {
|
||||
|
||||
@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);
|
||||
|
@ -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,
|
||||
|
58
pubspec.lock
58
pubspec.lock
@ -150,14 +150,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -393,7 +385,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@ -440,54 +432,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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.15+45
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0-100.2.beta
|
||||
@ -42,9 +42,6 @@ dependencies:
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_riverpod: ^2.6.1
|
||||
go_router: ^15.1.3
|
||||
flutter_downloader: ^1.12.0
|
||||
permission_handler: ^12.0.0+1
|
||||
path_provider: ^2.1.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -13,7 +13,7 @@ import 'package:f0ckapp/main.dart';
|
||||
void main() {
|
||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(const F0ckApp());
|
||||
await tester.pumpWidget(F0ckApp());
|
||||
|
||||
// Verify that our counter starts at 0.
|
||||
expect(find.text('0'), findsOneWidget);
|
||||
|
Reference in New Issue
Block a user