This commit is contained in:
		@@ -1,98 +0,0 @@
 | 
				
			|||||||
import 'dart:convert';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
					 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class AuthController extends GetxController {
 | 
					 | 
				
			||||||
  final EncryptedSharedPreferencesAsync storage =
 | 
					 | 
				
			||||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
					 | 
				
			||||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  RxnString token = RxnString();
 | 
					 | 
				
			||||||
  RxnInt userId = RxnInt();
 | 
					 | 
				
			||||||
  RxnString avatarUrl = RxnString();
 | 
					 | 
				
			||||||
  RxnString username = RxnString();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void onInit() {
 | 
					 | 
				
			||||||
    super.onInit();
 | 
					 | 
				
			||||||
    loadToken();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> loadToken() async {
 | 
					 | 
				
			||||||
    token.value = await storage.getString('token');
 | 
					 | 
				
			||||||
    if (token.value != null) {
 | 
					 | 
				
			||||||
      await fetchUserInfo();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> saveToken(String newToken) async {
 | 
					 | 
				
			||||||
    token.value = newToken;
 | 
					 | 
				
			||||||
    await storage.setString('token', newToken);
 | 
					 | 
				
			||||||
    await fetchUserInfo();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> logout() async {
 | 
					 | 
				
			||||||
    if (token.value != null) {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await http.post(
 | 
					 | 
				
			||||||
          Uri.parse('https://api.f0ck.me/logout'),
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            'Authorization': 'Bearer ${token.value}',
 | 
					 | 
				
			||||||
            'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        await mediaController.loadMediaItems();
 | 
					 | 
				
			||||||
        mediaController.mediaItems.refresh();
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        //
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    token.value = null;
 | 
					 | 
				
			||||||
    userId.value = null;
 | 
					 | 
				
			||||||
    avatarUrl.value = null;
 | 
					 | 
				
			||||||
    username.value = null;
 | 
					 | 
				
			||||||
    await storage.remove('token');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<bool> login(String username, String password) async {
 | 
					 | 
				
			||||||
    final http.Response response = await http.post(
 | 
					 | 
				
			||||||
      Uri.parse('https://api.f0ck.me/login'),
 | 
					 | 
				
			||||||
      headers: {'Content-Type': 'application/json'},
 | 
					 | 
				
			||||||
      body: json.encode({'username': username, 'password': password}),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    if (response.statusCode == 200) {
 | 
					 | 
				
			||||||
      final dynamic data = json.decode(response.body);
 | 
					 | 
				
			||||||
      if (data['token'] != null) {
 | 
					 | 
				
			||||||
        await saveToken(data['token']);
 | 
					 | 
				
			||||||
        await mediaController.loadMediaItems();
 | 
					 | 
				
			||||||
        mediaController.mediaItems.refresh();
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> fetchUserInfo() async {
 | 
					 | 
				
			||||||
    if (token.value == null) return;
 | 
					 | 
				
			||||||
    final http.Response response = await http.get(
 | 
					 | 
				
			||||||
      Uri.parse('https://api.f0ck.me/login/check'),
 | 
					 | 
				
			||||||
      headers: {'Authorization': 'Bearer ${token.value}'},
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    if (response.statusCode == 200) {
 | 
					 | 
				
			||||||
      final dynamic data = json.decode(response.body);
 | 
					 | 
				
			||||||
      userId.value = data['userid'] != null
 | 
					 | 
				
			||||||
          ? int.parse(data['userid'].toString())
 | 
					 | 
				
			||||||
          : null;
 | 
					 | 
				
			||||||
      avatarUrl.value = data['avatar'] != null
 | 
					 | 
				
			||||||
          ? 'https://f0ck.me/t/${data['avatar']}.webp'
 | 
					 | 
				
			||||||
          : null;
 | 
					 | 
				
			||||||
      username.value = data['user'];
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      await logout();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										114
									
								
								lib/controller/authcontroller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/controller/authcontroller.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
				
			|||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:encrypt_shared_preferences/provider.dart';
 | 
				
			||||||
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AuthController extends GetxController {
 | 
				
			||||||
 | 
					  final EncryptedSharedPreferencesAsync storage =
 | 
				
			||||||
 | 
					      EncryptedSharedPreferencesAsync.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RxnString token = RxnString();
 | 
				
			||||||
 | 
					  RxnInt userId = RxnInt();
 | 
				
			||||||
 | 
					  RxnString avatarUrl = RxnString();
 | 
				
			||||||
 | 
					  RxnString username = RxnString();
 | 
				
			||||||
 | 
					  RxBool isLoading = false.obs;
 | 
				
			||||||
 | 
					  RxnString error = RxnString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onInit() {
 | 
				
			||||||
 | 
					    super.onInit();
 | 
				
			||||||
 | 
					    loadToken();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> loadToken() async {
 | 
				
			||||||
 | 
					    token.value = await storage.getString('token');
 | 
				
			||||||
 | 
					    if (token.value != null) {
 | 
				
			||||||
 | 
					      await fetchUserInfo();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> saveToken(String newToken) async {
 | 
				
			||||||
 | 
					    token.value = newToken;
 | 
				
			||||||
 | 
					    await storage.setString('token', newToken);
 | 
				
			||||||
 | 
					    await fetchUserInfo();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> logout() async {
 | 
				
			||||||
 | 
					    if (token.value != null) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await http.post(
 | 
				
			||||||
 | 
					          Uri.parse('https://api.f0ck.me/logout'),
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            'Authorization': 'Bearer ${token.value}',
 | 
				
			||||||
 | 
					            'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } catch (_) {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    token.value = null;
 | 
				
			||||||
 | 
					    userId.value = null;
 | 
				
			||||||
 | 
					    avatarUrl.value = null;
 | 
				
			||||||
 | 
					    username.value = null;
 | 
				
			||||||
 | 
					    await storage.remove('token');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<bool> login(String username, String password) async {
 | 
				
			||||||
 | 
					    isLoading.value = true;
 | 
				
			||||||
 | 
					    error.value = null;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final http.Response response = await http.post(
 | 
				
			||||||
 | 
					        Uri.parse('https://api.f0ck.me/login'),
 | 
				
			||||||
 | 
					        headers: {'Content-Type': 'application/json'},
 | 
				
			||||||
 | 
					        body: json.encode({'username': username, 'password': password}),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (response.statusCode == 200) {
 | 
				
			||||||
 | 
					        final dynamic data = json.decode(response.body);
 | 
				
			||||||
 | 
					        if (data['token'] != null) {
 | 
				
			||||||
 | 
					          await saveToken(data['token']);
 | 
				
			||||||
 | 
					          userId.value = data['userid'];
 | 
				
			||||||
 | 
					          avatarUrl.value = data['avatar'] != null
 | 
				
			||||||
 | 
					              ? 'https://f0ck.me/t/${data['avatar']}.webp'
 | 
				
			||||||
 | 
					              : null;
 | 
				
			||||||
 | 
					          this.username.value = data['user'];
 | 
				
			||||||
 | 
					          return true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          error.value = 'Kein Token erhalten';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        error.value = 'Login fehlgeschlagen';
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      error.value = e.toString();
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      isLoading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> fetchUserInfo() async {
 | 
				
			||||||
 | 
					    if (token.value == null) return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final http.Response response = await http.get(
 | 
				
			||||||
 | 
					        Uri.parse('https://api.f0ck.me/login/check'),
 | 
				
			||||||
 | 
					        headers: {'Authorization': 'Bearer ${token.value}'},
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (response.statusCode == 200) {
 | 
				
			||||||
 | 
					        final dynamic data = json.decode(response.body);
 | 
				
			||||||
 | 
					        userId.value = data['userid'] != null
 | 
				
			||||||
 | 
					            ? int.tryParse(data['userid'].toString())
 | 
				
			||||||
 | 
					            : null;
 | 
				
			||||||
 | 
					        avatarUrl.value = data['avatar'] != null
 | 
				
			||||||
 | 
					            ? 'https://f0ck.me/t/${data['avatar']}.webp'
 | 
				
			||||||
 | 
					            : null;
 | 
				
			||||||
 | 
					        username.value = data['user'];
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        await logout();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (_) {
 | 
				
			||||||
 | 
					      await logout();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get isLoggedIn => token.value != null && token.value!.isNotEmpty;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,112 +0,0 @@
 | 
				
			|||||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
					 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/service/media_service.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MediaController extends GetxController {
 | 
					 | 
				
			||||||
  final EncryptedSharedPreferencesAsync storage =
 | 
					 | 
				
			||||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final RxList<MediaItem> mediaItems = <MediaItem>[].obs;
 | 
					 | 
				
			||||||
  final RxBool isLoading = false.obs;
 | 
					 | 
				
			||||||
  final RxString errorMessage = ''.obs;
 | 
					 | 
				
			||||||
  final MediaService _mediaService = MediaService();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  RxnString tag = RxnString();
 | 
					 | 
				
			||||||
  RxInt type = 0.obs;
 | 
					 | 
				
			||||||
  RxInt mode = 0.obs;
 | 
					 | 
				
			||||||
  RxBool random = false.obs;
 | 
					 | 
				
			||||||
  late RxBool muted = false.obs;
 | 
					 | 
				
			||||||
  late RxInt crossAxisCount = 0.obs;
 | 
					 | 
				
			||||||
  late RxBool drawerSwipeEnabled = true.obs;
 | 
					 | 
				
			||||||
  final Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  MediaItem? selectedItem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void onInit() async {
 | 
					 | 
				
			||||||
    super.onInit();
 | 
					 | 
				
			||||||
    await loadSettings();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> loadSettings() async {
 | 
					 | 
				
			||||||
    muted.value = await storage.getBoolean('muted') ?? false;
 | 
					 | 
				
			||||||
    crossAxisCount.value = await storage.getInt('crossAxisCount') ?? 0;
 | 
					 | 
				
			||||||
    drawerSwipeEnabled.value =
 | 
					 | 
				
			||||||
        await storage.getBoolean('drawerSwipeEnabled') ?? true;
 | 
					 | 
				
			||||||
    transitionType.value =
 | 
					 | 
				
			||||||
        PageTransition.values[await storage.getInt('transitionType') ?? 0];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> saveSettings() async {
 | 
					 | 
				
			||||||
    await storage.setBoolean('muted', muted.value);
 | 
					 | 
				
			||||||
    await storage.setInt('crossAxisCount', crossAxisCount.value);
 | 
					 | 
				
			||||||
    await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
 | 
					 | 
				
			||||||
    await storage.setInt('transitionType', transitionType.value.index);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setTag(String? newTag) async {
 | 
					 | 
				
			||||||
    tag.value = newTag;
 | 
					 | 
				
			||||||
    await loadMediaItems();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setType(int newType) async {
 | 
					 | 
				
			||||||
    type.value = newType;
 | 
					 | 
				
			||||||
    await loadMediaItems();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setMode(int newMode) async {
 | 
					 | 
				
			||||||
    mode.value = newMode;
 | 
					 | 
				
			||||||
    await loadMediaItems();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> toggleRandom() async {
 | 
					 | 
				
			||||||
    random.value = !random.value;
 | 
					 | 
				
			||||||
    await loadMediaItems();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> toggleMuted() async {
 | 
					 | 
				
			||||||
    muted.value = !muted.value;
 | 
					 | 
				
			||||||
    await saveSettings();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setCrossAxisCount(int newCrossAxisCount) async {
 | 
					 | 
				
			||||||
    crossAxisCount.value = newCrossAxisCount;
 | 
					 | 
				
			||||||
    await saveSettings();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setDrawerSwipeEnabled(bool newValue) async {
 | 
					 | 
				
			||||||
    drawerSwipeEnabled.value = newValue;
 | 
					 | 
				
			||||||
    await saveSettings();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> setTransitionType(PageTransition newType) async {
 | 
					 | 
				
			||||||
    transitionType.value = newType;
 | 
					 | 
				
			||||||
    await saveSettings();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> loadMediaItems({int? older, bool append = false}) async {
 | 
					 | 
				
			||||||
    if (isLoading.value) return;
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      isLoading.value = true;
 | 
					 | 
				
			||||||
      final List<MediaItem> items = await _mediaService.fetchMediaItems(
 | 
					 | 
				
			||||||
        type: type.value,
 | 
					 | 
				
			||||||
        mode: mode.value,
 | 
					 | 
				
			||||||
        random: random.value ? 1 : 0,
 | 
					 | 
				
			||||||
        tag: tag.value,
 | 
					 | 
				
			||||||
        older: older,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      append ? mediaItems.addAll(items) : mediaItems.assignAll(items);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      errorMessage.value = '';
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      errorMessage.value = 'Fehler beim Laden der Daten: ${e.toString()}';
 | 
					 | 
				
			||||||
      Get.snackbar('Error', e.toString());
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
      isLoading.value = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										181
									
								
								lib/controller/mediacontroller.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								lib/controller/mediacontroller.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
				
			|||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:encrypt_shared_preferences/provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/feed.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/services/api.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
				
			||||||
 | 
					const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MediaController extends GetxController {
 | 
				
			||||||
 | 
					  final ApiService _api = ApiService();
 | 
				
			||||||
 | 
					  final EncryptedSharedPreferencesAsync storage =
 | 
				
			||||||
 | 
					      EncryptedSharedPreferencesAsync.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RxList<MediaItem> items = <MediaItem>[].obs;
 | 
				
			||||||
 | 
					  RxBool loading = false.obs;
 | 
				
			||||||
 | 
					  RxBool atEnd = false.obs;
 | 
				
			||||||
 | 
					  RxBool atStart = false.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RxInt typeIndex = 0.obs;
 | 
				
			||||||
 | 
					  RxInt modeIndex = 0.obs;
 | 
				
			||||||
 | 
					  RxInt random = 0.obs;
 | 
				
			||||||
 | 
					  Rxn<String> tag = Rxn<String>();
 | 
				
			||||||
 | 
					  RxBool muted = false.obs;
 | 
				
			||||||
 | 
					  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
				
			||||||
 | 
					  RxBool drawerSwipeEnabled = true.obs;
 | 
				
			||||||
 | 
					  RxInt crossAxisCount = 0.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setTypeIndex(int idx) {
 | 
				
			||||||
 | 
					    typeIndex.value = idx;
 | 
				
			||||||
 | 
					    fetchInitial();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setModeIndex(int idx) {
 | 
				
			||||||
 | 
					    modeIndex.value = idx;
 | 
				
			||||||
 | 
					    fetchInitial();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setTag(String? newTag, {bool reload = true}) {
 | 
				
			||||||
 | 
					    tag.value = newTag;
 | 
				
			||||||
 | 
					    if (reload) {
 | 
				
			||||||
 | 
					      fetchInitial();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<MediaItem> get filteredItems {
 | 
				
			||||||
 | 
					    final String typeStr = mediaTypes[typeIndex.value];
 | 
				
			||||||
 | 
					    return items.where((item) {
 | 
				
			||||||
 | 
					      final bool typeOk = typeStr == "alles" || item.mime.startsWith(typeStr);
 | 
				
			||||||
 | 
					      return typeOk;
 | 
				
			||||||
 | 
					    }).toList();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<Favorite>?> toggleFavorite(
 | 
				
			||||||
 | 
					    MediaItem item,
 | 
				
			||||||
 | 
					    bool isFavorite,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      return await _api.toggleFavorite(item, isFavorite);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> fetchInitial({int? id}) async {
 | 
				
			||||||
 | 
					    loading.value = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final Feed result = await _api.fetchItems(
 | 
				
			||||||
 | 
					        type: typeIndex.value,
 | 
				
			||||||
 | 
					        mode: modeIndex.value,
 | 
				
			||||||
 | 
					        random: random.value,
 | 
				
			||||||
 | 
					        tag: tag.value,
 | 
				
			||||||
 | 
					        older: id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      items.assignAll(result.items);
 | 
				
			||||||
 | 
					      atEnd.value = result.atEnd;
 | 
				
			||||||
 | 
					      atStart.value = result.atStart;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      loading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> fetchMore() async {
 | 
				
			||||||
 | 
					    if (items.isEmpty || atEnd.value) return;
 | 
				
			||||||
 | 
					    loading.value = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final Feed result = await _api.fetchItems(
 | 
				
			||||||
 | 
					        older: items.last.id,
 | 
				
			||||||
 | 
					        type: typeIndex.value,
 | 
				
			||||||
 | 
					        mode: modeIndex.value,
 | 
				
			||||||
 | 
					        random: random.value,
 | 
				
			||||||
 | 
					        tag: tag.value,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      final List<MediaItem> newItems = result.items
 | 
				
			||||||
 | 
					          .where((item) => !items.any((existing) => existing.id == item.id))
 | 
				
			||||||
 | 
					          .toList();
 | 
				
			||||||
 | 
					      items.addAll(newItems);
 | 
				
			||||||
 | 
					      items.refresh();
 | 
				
			||||||
 | 
					      atEnd.value = result.atEnd;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      loading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<int> fetchNewer() async {
 | 
				
			||||||
 | 
					    if (items.isEmpty || atStart.value) return 0;
 | 
				
			||||||
 | 
					    loading.value = true;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final Feed result = await _api.fetchItems(
 | 
				
			||||||
 | 
					        newer: items.first.id,
 | 
				
			||||||
 | 
					        type: typeIndex.value,
 | 
				
			||||||
 | 
					        mode: modeIndex.value,
 | 
				
			||||||
 | 
					        random: random.value,
 | 
				
			||||||
 | 
					        tag: tag.value,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      int oldLength = filteredItems.length;
 | 
				
			||||||
 | 
					      final List<MediaItem> newItems = result.items
 | 
				
			||||||
 | 
					          .where((item) => !items.any((existing) => existing.id == item.id))
 | 
				
			||||||
 | 
					          .toList();
 | 
				
			||||||
 | 
					      items.insertAll(0, newItems);
 | 
				
			||||||
 | 
					      items.refresh();
 | 
				
			||||||
 | 
					      atStart.value = result.atStart;
 | 
				
			||||||
 | 
					      int newLength = filteredItems.length;
 | 
				
			||||||
 | 
					      return newLength - oldLength;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      loading.value = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void toggleMuted() {
 | 
				
			||||||
 | 
					    muted.value = !muted.value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setMuted(bool value) {
 | 
				
			||||||
 | 
					    muted.value = value;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setTransitionType(PageTransition type) async {
 | 
				
			||||||
 | 
					    transitionType.value = type;
 | 
				
			||||||
 | 
					    await saveSettings();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setCrossAxisCount(int value) async {
 | 
				
			||||||
 | 
					    crossAxisCount.value = value;
 | 
				
			||||||
 | 
					    await saveSettings();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> setDrawerSwipeEnabled(bool enabled) async {
 | 
				
			||||||
 | 
					    drawerSwipeEnabled.value = enabled;
 | 
				
			||||||
 | 
					    await saveSettings();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void toggleRandom() {
 | 
				
			||||||
 | 
					    random.value = random.value == 1 ? 0 : 1;
 | 
				
			||||||
 | 
					    fetchInitial();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void onInit() async {
 | 
				
			||||||
 | 
					    super.onInit();
 | 
				
			||||||
 | 
					    await loadSettings();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> loadSettings() async {
 | 
				
			||||||
 | 
					    muted.value = await storage.getBoolean('muted') ?? false;
 | 
				
			||||||
 | 
					    crossAxisCount.value = await storage.getInt('crossAxisCount') ?? 0;
 | 
				
			||||||
 | 
					    drawerSwipeEnabled.value =
 | 
				
			||||||
 | 
					        await storage.getBoolean('drawerSwipeEnabled') ?? true;
 | 
				
			||||||
 | 
					    transitionType.value =
 | 
				
			||||||
 | 
					        PageTransition.values[await storage.getInt('transitionType') ?? 0];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> saveSettings() async {
 | 
				
			||||||
 | 
					    await storage.setBoolean('muted', muted.value);
 | 
				
			||||||
 | 
					    await storage.setInt('crossAxisCount', crossAxisCount.value);
 | 
				
			||||||
 | 
					    await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
 | 
				
			||||||
 | 
					    await storage.setInt('transitionType', transitionType.value.index);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,29 +1,28 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
					import 'package:encrypt_shared_preferences/provider.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/service/media_service.dart';
 | 
					import 'package:f0ckapp/controller/authcontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/localization_controller.dart';
 | 
					import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/controller/themecontroller.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/mediadetail.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/appversion.dart';
 | 
					import 'package:f0ckapp/utils/appversion.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/theme_controller.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/screens/mediagrid.dart';
 | 
				
			||||||
import 'package:f0ckapp/screens/detail_view.dart';
 | 
					import 'package:f0ckapp/screens/login.dart';
 | 
				
			||||||
import 'package:f0ckapp/screens/media_grid.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() async {
 | 
					void main() async {
 | 
				
			||||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
					  WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
 | 
					 | 
				
			||||||
  await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
 | 
					  await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
 | 
				
			||||||
  await MyTranslations.loadTranslations();
 | 
					  await MyTranslations.loadTranslations();
 | 
				
			||||||
  await AppVersion.init();
 | 
					  await AppVersion.init();
 | 
				
			||||||
  Get.put(MediaService());
 | 
					 | 
				
			||||||
  Get.put(MediaController());
 | 
					 | 
				
			||||||
  Get.put(AuthController());
 | 
					  Get.put(AuthController());
 | 
				
			||||||
  LocalizationController localizationController = Get.put(LocalizationController());
 | 
					  final MediaController mediaController = Get.put(MediaController());
 | 
				
			||||||
  final ThemeController themeController = Get.put(ThemeController());
 | 
					  final ThemeController themeController = Get.put(ThemeController());
 | 
				
			||||||
 | 
					  final LocalizationController localizationController = Get.put(
 | 
				
			||||||
 | 
					    LocalizationController(),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Get.addTranslations(MyTranslations.instance.keys);
 | 
					  Get.addTranslations(MyTranslations.instance.keys);
 | 
				
			||||||
  Get.locale = localizationController.currentLocale.value;
 | 
					  Get.locale = localizationController.currentLocale.value;
 | 
				
			||||||
@@ -37,16 +36,36 @@ void main() async {
 | 
				
			|||||||
        navigatorKey: Get.key,
 | 
					        navigatorKey: Get.key,
 | 
				
			||||||
        theme: themeController.currentTheme.value,
 | 
					        theme: themeController.currentTheme.value,
 | 
				
			||||||
        debugShowCheckedModeBanner: false,
 | 
					        debugShowCheckedModeBanner: false,
 | 
				
			||||||
 | 
					        initialRoute: '/',
 | 
				
			||||||
        onGenerateRoute: (RouteSettings settings) {
 | 
					        onGenerateRoute: (RouteSettings settings) {
 | 
				
			||||||
          final uri = Uri.parse(settings.name ?? '/');
 | 
					          final Uri uri = Uri.parse(settings.name ?? '/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (uri.path == '/' || uri.pathSegments.isEmpty) {
 | 
					          if (uri.path == '/' || uri.pathSegments.isEmpty) {
 | 
				
			||||||
            return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
					            return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (uri.path == '/login') {
 | 
				
			||||||
 | 
					            return MaterialPageRoute(builder: (_) => LoginScreen());
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (uri.pathSegments.length == 1) {
 | 
					          if (uri.pathSegments.length == 1) {
 | 
				
			||||||
            final int id = int.parse(uri.pathSegments.first);
 | 
					            final int id = int.parse(uri.pathSegments.first);
 | 
				
			||||||
            return MaterialPageRoute(builder: (_) => DetailView(initialId: id));
 | 
					
 | 
				
			||||||
 | 
					            return MaterialPageRoute(
 | 
				
			||||||
 | 
					              builder: (_) => FutureBuilder(
 | 
				
			||||||
 | 
					                future: mediaController.items.isEmpty
 | 
				
			||||||
 | 
					                    ? mediaController.fetchInitial(id: id)
 | 
				
			||||||
 | 
					                    : Future.value(),
 | 
				
			||||||
 | 
					                builder: (context, snapshot) {
 | 
				
			||||||
 | 
					                  if (snapshot.connectionState == ConnectionState.done) {
 | 
				
			||||||
 | 
					                    return MediaDetailScreen(initialId: id);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                  return const Scaffold(
 | 
				
			||||||
 | 
					                    body: Center(child: CircularProgressIndicator()),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
					          return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								lib/models/feed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/models/feed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Feed {
 | 
				
			||||||
 | 
					  final bool atEnd;
 | 
				
			||||||
 | 
					  final bool atStart;
 | 
				
			||||||
 | 
					  final List<MediaItem> items;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Feed({required this.atEnd, required this.atStart, required this.items});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory Feed.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
 | 
					    return Feed(
 | 
				
			||||||
 | 
					      atEnd: json['atEnd'] ?? false,
 | 
				
			||||||
 | 
					      atStart: json['atStart'] ?? false,
 | 
				
			||||||
 | 
					      items: (json['items'] as List).map((e) => MediaItem.fromJson(e)).toList(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -5,7 +5,7 @@ class MediaItem {
 | 
				
			|||||||
  final int stamp;
 | 
					  final int stamp;
 | 
				
			||||||
  final String dest;
 | 
					  final String dest;
 | 
				
			||||||
  final int mode;
 | 
					  final int mode;
 | 
				
			||||||
  final List<Tag> tags;
 | 
					  final List<Tag>? tags;
 | 
				
			||||||
  final List<Favorite>? favorites;
 | 
					  final List<Favorite>? favorites;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MediaItem({
 | 
					  MediaItem({
 | 
				
			||||||
@@ -15,10 +15,15 @@ class MediaItem {
 | 
				
			|||||||
    required this.stamp,
 | 
					    required this.stamp,
 | 
				
			||||||
    required this.dest,
 | 
					    required this.dest,
 | 
				
			||||||
    required this.mode,
 | 
					    required this.mode,
 | 
				
			||||||
    required this.tags,
 | 
					    this.tags = const [],
 | 
				
			||||||
    required this.favorites,
 | 
					    this.favorites = const [],
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  String get thumbnailUrl => 'https://f0ck.me/t/$id.webp';
 | 
				
			||||||
 | 
					  String get mediaUrl => 'https://f0ck.me/b/$dest';
 | 
				
			||||||
 | 
					  String get coverUrl => 'https://f0ck.me/ca/$id.webp';
 | 
				
			||||||
 | 
					  String get postUrl => 'https://f0ck.me/$id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  MediaItem copyWith({
 | 
					  MediaItem copyWith({
 | 
				
			||||||
    int? id,
 | 
					    int? id,
 | 
				
			||||||
    String? mime,
 | 
					    String? mime,
 | 
				
			||||||
@@ -42,27 +47,6 @@ class MediaItem {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory MediaItem.fromJson(Map<String, dynamic> json) {
 | 
					  factory MediaItem.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
    List<Tag> parsedTags = [];
 | 
					 | 
				
			||||||
    if (json['tags'] is List) {
 | 
					 | 
				
			||||||
      parsedTags = (json['tags'] as List<dynamic>)
 | 
					 | 
				
			||||||
          .map((tagJson) => Tag.fromJson(tagJson as Map<String, dynamic>))
 | 
					 | 
				
			||||||
          .toList();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      parsedTags = [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    List<Favorite> parsedFavorites = [];
 | 
					 | 
				
			||||||
    if (json['favorites'] is List) {
 | 
					 | 
				
			||||||
      parsedFavorites = (json['favorites'] as List<dynamic>)
 | 
					 | 
				
			||||||
          .map(
 | 
					 | 
				
			||||||
            (favoritesJson) =>
 | 
					 | 
				
			||||||
                Favorite.fromJson(favoritesJson as Map<String, dynamic>),
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
          .toList();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      parsedFavorites = [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return MediaItem(
 | 
					    return MediaItem(
 | 
				
			||||||
      id: json['id'],
 | 
					      id: json['id'],
 | 
				
			||||||
      mime: json['mime'],
 | 
					      mime: json['mime'],
 | 
				
			||||||
@@ -70,15 +54,18 @@ class MediaItem {
 | 
				
			|||||||
      stamp: json['stamp'],
 | 
					      stamp: json['stamp'],
 | 
				
			||||||
      dest: json['dest'],
 | 
					      dest: json['dest'],
 | 
				
			||||||
      mode: json['mode'],
 | 
					      mode: json['mode'],
 | 
				
			||||||
      tags: parsedTags,
 | 
					      tags:
 | 
				
			||||||
      favorites: parsedFavorites,
 | 
					          (json['tags'] as List<dynamic>?)
 | 
				
			||||||
 | 
					              ?.map((e) => Tag.fromJson(e))
 | 
				
			||||||
 | 
					              .toList() ??
 | 
				
			||||||
 | 
					          [],
 | 
				
			||||||
 | 
					      favorites:
 | 
				
			||||||
 | 
					          (json['favorites'] as List<dynamic>?)
 | 
				
			||||||
 | 
					              ?.map((e) => Favorite.fromJson(e))
 | 
				
			||||||
 | 
					              .toList() ??
 | 
				
			||||||
 | 
					          [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  String get thumbnailUrl => 'https://f0ck.me/t/$id.webp';
 | 
					 | 
				
			||||||
  String get mediaUrl => 'https://f0ck.me/b/$dest';
 | 
					 | 
				
			||||||
  String get coverUrl => 'https://f0ck.me/ca/$id.webp';
 | 
					 | 
				
			||||||
  String get postUrl => 'https://f0ck.me/$id';
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Tag {
 | 
					class Tag {
 | 
				
			||||||
@@ -99,15 +86,19 @@ class Tag {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class Favorite {
 | 
					class Favorite {
 | 
				
			||||||
  final int userId;
 | 
					  final int userId;
 | 
				
			||||||
  final String user;
 | 
					  final String username;
 | 
				
			||||||
  final int avatar;
 | 
					  final int avatar;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Favorite({required this.userId, required this.user, required this.avatar});
 | 
					  Favorite({
 | 
				
			||||||
 | 
					    required this.userId,
 | 
				
			||||||
 | 
					    required this.username,
 | 
				
			||||||
 | 
					    required this.avatar,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory Favorite.fromJson(Map<String, dynamic> json) {
 | 
					  factory Favorite.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
    return Favorite(
 | 
					    return Favorite(
 | 
				
			||||||
      userId: json['user_id'],
 | 
					      userId: json['user_id'],
 | 
				
			||||||
      user: json['user'],
 | 
					      username: json['user'],
 | 
				
			||||||
      avatar: json['avatar'],
 | 
					      avatar: json['avatar'],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -1,293 +0,0 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
					 | 
				
			||||||
import 'package:share_plus/share_plus.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/service/media_service.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/utils/smartrefreshindicator.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/screens/media_grid.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/screens/fullscreen_screen.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/detailmediacontent.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DetailView extends StatefulWidget {
 | 
					 | 
				
			||||||
  final int initialId;
 | 
					 | 
				
			||||||
  const DetailView({super.key, required this.initialId});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  State<DetailView> createState() => _DetailViewState();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _DetailViewState extends State<DetailView> {
 | 
					 | 
				
			||||||
  final MediaController controller = Get.find<MediaController>();
 | 
					 | 
				
			||||||
  MediaItem? item;
 | 
					 | 
				
			||||||
  bool isLoading = false;
 | 
					 | 
				
			||||||
  PageController? _pageController;
 | 
					 | 
				
			||||||
  int _currentPage = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void initState() {
 | 
					 | 
				
			||||||
    super.initState();
 | 
					 | 
				
			||||||
    _setupInitialView();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    ever(controller.drawerSwipeEnabled, (_) {
 | 
					 | 
				
			||||||
      setState(() {});
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void dispose() {
 | 
					 | 
				
			||||||
    _pageController?.dispose();
 | 
					 | 
				
			||||||
    super.dispose();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _setupInitialView() async {
 | 
					 | 
				
			||||||
    bool itemExists = controller.mediaItems.any(
 | 
					 | 
				
			||||||
      (media) => media.id == widget.initialId,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!itemExists) {
 | 
					 | 
				
			||||||
      await _initializeDetail(widget.initialId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    _initializePageController();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _initializePageController() {
 | 
					 | 
				
			||||||
    final page = controller.mediaItems.indexWhere(
 | 
					 | 
				
			||||||
      (media) => media.id == widget.initialId,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    setState(() {
 | 
					 | 
				
			||||||
      _currentPage = page < 0 ? 0 : page;
 | 
					 | 
				
			||||||
      _pageController = PageController(initialPage: _currentPage)
 | 
					 | 
				
			||||||
        ..addListener(() {
 | 
					 | 
				
			||||||
          setState(() => _currentPage = _pageController!.page!.round());
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _downloadMedia(MediaItem item) async {
 | 
					 | 
				
			||||||
    final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
 | 
					 | 
				
			||||||
    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
					 | 
				
			||||||
      'filePath': file.path,
 | 
					 | 
				
			||||||
      'fileName': item.dest,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    success == true
 | 
					 | 
				
			||||||
        ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
					 | 
				
			||||||
        : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _showMsg(String message) {
 | 
					 | 
				
			||||||
    if (!mounted) return;
 | 
					 | 
				
			||||||
    Get
 | 
					 | 
				
			||||||
      ..closeAllSnackbars()
 | 
					 | 
				
			||||||
      ..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _initializeDetail(int deepLinkId) async {
 | 
					 | 
				
			||||||
    item = controller.mediaItems.firstWhereOrNull(
 | 
					 | 
				
			||||||
      (element) => element.id == deepLinkId,
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (item == null) {
 | 
					 | 
				
			||||||
      setState(() => isLoading = true);
 | 
					 | 
				
			||||||
      await controller.loadMediaItems(older: deepLinkId + 50);
 | 
					 | 
				
			||||||
      item = controller.mediaItems.firstWhereOrNull(
 | 
					 | 
				
			||||||
        (element) => element.id == deepLinkId,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (item == null) {
 | 
					 | 
				
			||||||
        Get.offAll(() => const MediaGrid());
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      setState(() => isLoading = false);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    final MediaService mediaService = Get.find<MediaService>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isLoading || controller.mediaItems.isEmpty || _pageController == null) {
 | 
					 | 
				
			||||||
      return Scaffold(
 | 
					 | 
				
			||||||
        appBar: AppBar(
 | 
					 | 
				
			||||||
          title: const Text("f0ck"),
 | 
					 | 
				
			||||||
          leading: Navigator.canPop(context)
 | 
					 | 
				
			||||||
              ? null
 | 
					 | 
				
			||||||
              : IconButton(
 | 
					 | 
				
			||||||
                  icon: const Icon(Icons.arrow_back),
 | 
					 | 
				
			||||||
                  onPressed: () {
 | 
					 | 
				
			||||||
                    Get.offAll(() => const MediaGrid());
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        body: const Center(child: CircularProgressIndicator()),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final MediaItem currentItem = controller.mediaItems[_currentPage];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Scaffold(
 | 
					 | 
				
			||||||
      endDrawer: const EndDrawer(),
 | 
					 | 
				
			||||||
      endDrawerEnableOpenDragGesture: controller.drawerSwipeEnabled.value,
 | 
					 | 
				
			||||||
      persistentFooterButtons: controller.tag.value != null
 | 
					 | 
				
			||||||
          ? [
 | 
					 | 
				
			||||||
              Center(
 | 
					 | 
				
			||||||
                child: InputChip(
 | 
					 | 
				
			||||||
                  label: Text(controller.tag.value!),
 | 
					 | 
				
			||||||
                  onDeleted: () {
 | 
					 | 
				
			||||||
                    controller.setTag(null);
 | 
					 | 
				
			||||||
                    Get.offAll(() => const MediaGrid());
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
          : null,
 | 
					 | 
				
			||||||
      body: CustomScrollView(
 | 
					 | 
				
			||||||
        slivers: [
 | 
					 | 
				
			||||||
          SliverAppBar(
 | 
					 | 
				
			||||||
            floating: true,
 | 
					 | 
				
			||||||
            pinned: true,
 | 
					 | 
				
			||||||
            snap: true,
 | 
					 | 
				
			||||||
            centerTitle: true,
 | 
					 | 
				
			||||||
            title: Text('f0ck #${currentItem.id.toString()}'),
 | 
					 | 
				
			||||||
            leading: IconButton(
 | 
					 | 
				
			||||||
              icon: const Icon(Icons.arrow_back),
 | 
					 | 
				
			||||||
              onPressed: () {
 | 
					 | 
				
			||||||
                Navigator.popUntil(
 | 
					 | 
				
			||||||
                  context,
 | 
					 | 
				
			||||||
                  (route) => route.settings.name == '/' || route.isFirst,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            actions: [
 | 
					 | 
				
			||||||
              IconButton(
 | 
					 | 
				
			||||||
                icon: const Icon(Icons.fullscreen),
 | 
					 | 
				
			||||||
                onPressed: () {
 | 
					 | 
				
			||||||
                  Get.to(
 | 
					 | 
				
			||||||
                    FullScreenMediaView(item: currentItem),
 | 
					 | 
				
			||||||
                    fullscreenDialog: true,
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              IconButton(
 | 
					 | 
				
			||||||
                icon: const Icon(Icons.download),
 | 
					 | 
				
			||||||
                onPressed: () async {
 | 
					 | 
				
			||||||
                  await _downloadMedia(currentItem);
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              PopupMenuButton<String>(
 | 
					 | 
				
			||||||
                onSelected: (value) async {
 | 
					 | 
				
			||||||
                  switch (value) {
 | 
					 | 
				
			||||||
                    case 'media':
 | 
					 | 
				
			||||||
                      File file = await DefaultCacheManager().getSingleFile(
 | 
					 | 
				
			||||||
                        currentItem.mediaUrl,
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                      Uint8List bytes = await file.readAsBytes();
 | 
					 | 
				
			||||||
                      final params = ShareParams(
 | 
					 | 
				
			||||||
                        files: [
 | 
					 | 
				
			||||||
                          XFile.fromData(bytes, mimeType: currentItem.mime),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                      await SharePlus.instance.share(params);
 | 
					 | 
				
			||||||
                      break;
 | 
					 | 
				
			||||||
                    case 'direct_link':
 | 
					 | 
				
			||||||
                      await SharePlus.instance.share(
 | 
					 | 
				
			||||||
                        ShareParams(text: currentItem.mediaUrl),
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                      break;
 | 
					 | 
				
			||||||
                    case 'post_link':
 | 
					 | 
				
			||||||
                      await SharePlus.instance.share(
 | 
					 | 
				
			||||||
                        ShareParams(text: currentItem.postUrl),
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                      break;
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                itemBuilder: (context) => [
 | 
					 | 
				
			||||||
                  PopupMenuItem(
 | 
					 | 
				
			||||||
                    value: 'media',
 | 
					 | 
				
			||||||
                    child: ListTile(
 | 
					 | 
				
			||||||
                      leading: const Icon(Icons.image),
 | 
					 | 
				
			||||||
                      title: const Text('Als Datei'),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  PopupMenuItem(
 | 
					 | 
				
			||||||
                    value: 'direct_link',
 | 
					 | 
				
			||||||
                    child: ListTile(
 | 
					 | 
				
			||||||
                      leading: const Icon(Icons.link),
 | 
					 | 
				
			||||||
                      title: const Text('Link zur Datei'),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  PopupMenuItem(
 | 
					 | 
				
			||||||
                    value: 'post_link',
 | 
					 | 
				
			||||||
                    child: ListTile(
 | 
					 | 
				
			||||||
                      leading: const Icon(Icons.article),
 | 
					 | 
				
			||||||
                      title: const Text('Link zum f0ck'),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                icon: const Icon(Icons.share),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              Builder(
 | 
					 | 
				
			||||||
                builder: (context) => IconButton(
 | 
					 | 
				
			||||||
                  icon: const Icon(Icons.menu),
 | 
					 | 
				
			||||||
                  onPressed: () {
 | 
					 | 
				
			||||||
                    Scaffold.of(context).openEndDrawer();
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          SliverFillRemaining(
 | 
					 | 
				
			||||||
            child: PageView.builder(
 | 
					 | 
				
			||||||
              controller: _pageController,
 | 
					 | 
				
			||||||
              itemCount: controller.mediaItems.length,
 | 
					 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					 | 
				
			||||||
                final MediaItem pageItem = controller.mediaItems[index];
 | 
					 | 
				
			||||||
                return AnimatedBuilder(
 | 
					 | 
				
			||||||
                  animation: _pageController!,
 | 
					 | 
				
			||||||
                  builder: (context, child) {
 | 
					 | 
				
			||||||
                    return buildAnimatedTransition(
 | 
					 | 
				
			||||||
                      context: context,
 | 
					 | 
				
			||||||
                      child: child!,
 | 
					 | 
				
			||||||
                      pageController: _pageController!,
 | 
					 | 
				
			||||||
                      index: index,
 | 
					 | 
				
			||||||
                      controller: controller,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  child: SmartRefreshIndicator(
 | 
					 | 
				
			||||||
                    onRefresh: () async {
 | 
					 | 
				
			||||||
                      final MediaItem? refreshed = await mediaService.fetchItem(
 | 
					 | 
				
			||||||
                        pageItem.id,
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                      if (refreshed != null) {
 | 
					 | 
				
			||||||
                        controller.mediaItems[index] = refreshed;
 | 
					 | 
				
			||||||
                        controller.mediaItems.refresh();
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    child: DetailMediaContent(
 | 
					 | 
				
			||||||
                      currentPage: _currentPage,
 | 
					 | 
				
			||||||
                      index: index,
 | 
					 | 
				
			||||||
                      onTagTap: (tag) {
 | 
					 | 
				
			||||||
                        if (tag == 'sfw' || tag == 'nsfw') return;
 | 
					 | 
				
			||||||
                        controller.setTag(tag);
 | 
					 | 
				
			||||||
                        Get.offAllNamed('/');
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
					import 'package:f0ckapp/widgets/video_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FullScreenMediaView extends StatefulWidget {
 | 
					class FullScreenMediaView extends StatefulWidget {
 | 
				
			||||||
@@ -1,17 +1,18 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
					import 'package:f0ckapp/controller/authcontroller.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LoginPage extends StatefulWidget {
 | 
					class LoginScreen extends StatefulWidget {
 | 
				
			||||||
  const LoginPage({super.key});
 | 
					  const LoginScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<StatefulWidget> createState() => _LoginPageState();
 | 
					  State<LoginScreen> createState() => _LoginScreenState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _LoginPageState extends State<LoginPage> {
 | 
					class _LoginScreenState extends State<LoginScreen> {
 | 
				
			||||||
  final AuthController authController = Get.find();
 | 
					  final AuthController authController = Get.put(AuthController());
 | 
				
			||||||
  final TextEditingController usernameController = TextEditingController();
 | 
					  final TextEditingController usernameController = TextEditingController();
 | 
				
			||||||
  final TextEditingController passwordController = TextEditingController();
 | 
					  final TextEditingController passwordController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,7 +21,7 @@ class _LoginPageState extends State<LoginPage> {
 | 
				
			|||||||
  void _showMsg(String message, {String title = ''}) {
 | 
					  void _showMsg(String message, {String title = ''}) {
 | 
				
			||||||
    Get
 | 
					    Get
 | 
				
			||||||
      ..closeAllSnackbars()
 | 
					      ..closeAllSnackbars()
 | 
				
			||||||
      ..snackbar(message, title, snackPosition: SnackPosition.BOTTOM);
 | 
					      ..snackbar(title, message, snackPosition: SnackPosition.BOTTOM);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -80,6 +81,11 @@ class _LoginPageState extends State<LoginPage> {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const SizedBox(height: 24),
 | 
					                  const SizedBox(height: 24),
 | 
				
			||||||
 | 
					                  if (authController.error.value != null)
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      authController.error.value!,
 | 
				
			||||||
 | 
					                      style: const TextStyle(color: Colors.red),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                  SizedBox(
 | 
					                  SizedBox(
 | 
				
			||||||
                    width: double.infinity,
 | 
					                    width: double.infinity,
 | 
				
			||||||
                    child: ElevatedButton(
 | 
					                    child: ElevatedButton(
 | 
				
			||||||
@@ -87,16 +93,23 @@ class _LoginPageState extends State<LoginPage> {
 | 
				
			|||||||
                          ? null
 | 
					                          ? null
 | 
				
			||||||
                          : () async {
 | 
					                          : () async {
 | 
				
			||||||
                              setState(() => _isLoading = true);
 | 
					                              setState(() => _isLoading = true);
 | 
				
			||||||
                              final success = await authController.login(
 | 
					                              final bool success = await authController.login(
 | 
				
			||||||
                                usernameController.text,
 | 
					                                usernameController.text,
 | 
				
			||||||
                                passwordController.text,
 | 
					                                passwordController.text,
 | 
				
			||||||
                              );
 | 
					                              );
 | 
				
			||||||
                              setState(() => _isLoading = false);
 | 
					                              setState(() => _isLoading = false);
 | 
				
			||||||
                              if (!success) {
 | 
					                              if (!success) {
 | 
				
			||||||
                                return _showMsg('Login fehlgeschlagen!');
 | 
					                                _showMsg(
 | 
				
			||||||
 | 
					                                  'Login fehlgeschlagen!',
 | 
				
			||||||
 | 
					                                  title: 'Fehler',
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                              } else {
 | 
				
			||||||
 | 
					                                Get.offAllNamed('/');
 | 
				
			||||||
 | 
					                                _showMsg(
 | 
				
			||||||
 | 
					                                  'Erfolgreich eingeloggt.',
 | 
				
			||||||
 | 
					                                  title: 'Login',
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
                              }
 | 
					                              }
 | 
				
			||||||
                              _showMsg('Erfolgreich eingeloggt.');
 | 
					 | 
				
			||||||
                              Get.back();
 | 
					 | 
				
			||||||
                            },
 | 
					                            },
 | 
				
			||||||
                      child: _isLoading
 | 
					                      child: _isLoading
 | 
				
			||||||
                          ? const SizedBox(
 | 
					                          ? const SizedBox(
 | 
				
			||||||
							
								
								
									
										312
									
								
								lib/screens/mediadetail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								lib/screens/mediadetail.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,312 @@
 | 
				
			|||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_cache_manager/file.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:share_plus/share_plus.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/controller/authcontroller.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/actiontag.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/favorite_avatars.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/fullscreen.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/widgets/video_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MediaDetailScreen extends StatefulWidget {
 | 
				
			||||||
 | 
					  final int initialId;
 | 
				
			||||||
 | 
					  const MediaDetailScreen({super.key, required this.initialId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<MediaDetailScreen> createState() => _MediaDetailScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			||||||
 | 
					  late PageController _pageController;
 | 
				
			||||||
 | 
					  final MediaController mediaController = Get.find<MediaController>();
 | 
				
			||||||
 | 
					  final AuthController authController = Get.find<AuthController>();
 | 
				
			||||||
 | 
					  int? _currentIndex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    final int idx = mediaController.items.indexWhere(
 | 
				
			||||||
 | 
					      (item) => item.id == widget.initialId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    _currentIndex = idx >= 0 ? idx : 0;
 | 
				
			||||||
 | 
					    _pageController = PageController(initialPage: _currentIndex!);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _showMsg(String message) {
 | 
				
			||||||
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					    Get
 | 
				
			||||||
 | 
					      ..closeAllSnackbars()
 | 
				
			||||||
 | 
					      ..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onPageChanged(int idx) {
 | 
				
			||||||
 | 
					    if (idx != _currentIndex) {
 | 
				
			||||||
 | 
					      setState(() => _currentIndex = idx);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (idx >= mediaController.items.length - 2 &&
 | 
				
			||||||
 | 
					        !mediaController.loading.value &&
 | 
				
			||||||
 | 
					        !mediaController.atEnd.value) {
 | 
				
			||||||
 | 
					      mediaController.fetchMore();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (idx <= 1 &&
 | 
				
			||||||
 | 
					        !mediaController.loading.value &&
 | 
				
			||||||
 | 
					        !mediaController.atStart.value) {
 | 
				
			||||||
 | 
					      mediaController.fetchNewer();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _downloadMedia(MediaItem item) async {
 | 
				
			||||||
 | 
					    final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
 | 
				
			||||||
 | 
					    final MethodChannel methodChannel = const MethodChannel('MediaShit');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
 | 
				
			||||||
 | 
					      'filePath': file.path,
 | 
				
			||||||
 | 
					      'fileName': item.dest,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    success == true
 | 
				
			||||||
 | 
					        ? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
 | 
				
			||||||
 | 
					        : _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _pageController.dispose();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Widget _buildMedia(MediaItem item, bool isActive) {
 | 
				
			||||||
 | 
					    if (item.mime.startsWith('image/')) {
 | 
				
			||||||
 | 
					      return CachedNetworkImage(
 | 
				
			||||||
 | 
					        imageUrl: item.mediaUrl,
 | 
				
			||||||
 | 
					        fit: BoxFit.contain,
 | 
				
			||||||
 | 
					        errorWidget: (c, e, s) => const Icon(Icons.broken_image, size: 100),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (item.mime.startsWith('video/') ||
 | 
				
			||||||
 | 
					        item.mime.startsWith('audio/')) {
 | 
				
			||||||
 | 
					      return VideoWidget(details: item, isActive: isActive);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return const Icon(Icons.help_outline, size: 100);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Obx(
 | 
				
			||||||
 | 
					      () => PageView.builder(
 | 
				
			||||||
 | 
					        controller: _pageController,
 | 
				
			||||||
 | 
					        itemCount: mediaController.items.length,
 | 
				
			||||||
 | 
					        onPageChanged: _onPageChanged,
 | 
				
			||||||
 | 
					        itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					          final MediaItem item = mediaController.items[index];
 | 
				
			||||||
 | 
					          final bool isActive = index == _currentIndex;
 | 
				
			||||||
 | 
					          final bool isFavorite =
 | 
				
			||||||
 | 
					              item.favorites?.any(
 | 
				
			||||||
 | 
					                (f) => f.userId == authController.userId.value,
 | 
				
			||||||
 | 
					              ) ??
 | 
				
			||||||
 | 
					              false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return Scaffold(
 | 
				
			||||||
 | 
					            endDrawer: EndDrawer(),
 | 
				
			||||||
 | 
					            endDrawerEnableOpenDragGesture:
 | 
				
			||||||
 | 
					                mediaController.drawerSwipeEnabled.value,
 | 
				
			||||||
 | 
					            body: CustomScrollView(
 | 
				
			||||||
 | 
					              slivers: [
 | 
				
			||||||
 | 
					                SliverAppBar(
 | 
				
			||||||
 | 
					                  floating: false,
 | 
				
			||||||
 | 
					                  pinned: true,
 | 
				
			||||||
 | 
					                  title: Text('f0ck #${item.id}'),
 | 
				
			||||||
 | 
					                  actions: [
 | 
				
			||||||
 | 
					                    IconButton(
 | 
				
			||||||
 | 
					                      icon: const Icon(Icons.fullscreen),
 | 
				
			||||||
 | 
					                      onPressed: () {
 | 
				
			||||||
 | 
					                        Get.to(
 | 
				
			||||||
 | 
					                          FullScreenMediaView(item: item),
 | 
				
			||||||
 | 
					                          fullscreenDialog: true,
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    IconButton(
 | 
				
			||||||
 | 
					                      icon: const Icon(Icons.download),
 | 
				
			||||||
 | 
					                      onPressed: () async {
 | 
				
			||||||
 | 
					                        await _downloadMedia(item);
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    PopupMenuButton<String>(
 | 
				
			||||||
 | 
					                      onSelected: (value) async {
 | 
				
			||||||
 | 
					                        switch (value) {
 | 
				
			||||||
 | 
					                          case 'media':
 | 
				
			||||||
 | 
					                            File file = await DefaultCacheManager()
 | 
				
			||||||
 | 
					                                .getSingleFile(item.mediaUrl);
 | 
				
			||||||
 | 
					                            Uint8List bytes = await file.readAsBytes();
 | 
				
			||||||
 | 
					                            final params = ShareParams(
 | 
				
			||||||
 | 
					                              files: [
 | 
				
			||||||
 | 
					                                XFile.fromData(bytes, mimeType: item.mime),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                            await SharePlus.instance.share(params);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                          case 'direct_link':
 | 
				
			||||||
 | 
					                            await SharePlus.instance.share(
 | 
				
			||||||
 | 
					                              ShareParams(text: item.mediaUrl),
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                          case 'post_link':
 | 
				
			||||||
 | 
					                            await SharePlus.instance.share(
 | 
				
			||||||
 | 
					                              ShareParams(text: item.postUrl),
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      itemBuilder: (context) => [
 | 
				
			||||||
 | 
					                        PopupMenuItem(
 | 
				
			||||||
 | 
					                          value: 'media',
 | 
				
			||||||
 | 
					                          child: ListTile(
 | 
				
			||||||
 | 
					                            leading: const Icon(Icons.image),
 | 
				
			||||||
 | 
					                            title: const Text('Als Datei'),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        PopupMenuItem(
 | 
				
			||||||
 | 
					                          value: 'direct_link',
 | 
				
			||||||
 | 
					                          child: ListTile(
 | 
				
			||||||
 | 
					                            leading: const Icon(Icons.link),
 | 
				
			||||||
 | 
					                            title: const Text('Link zur Datei'),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        PopupMenuItem(
 | 
				
			||||||
 | 
					                          value: 'post_link',
 | 
				
			||||||
 | 
					                          child: ListTile(
 | 
				
			||||||
 | 
					                            leading: const Icon(Icons.article),
 | 
				
			||||||
 | 
					                            title: const Text('Link zum f0ck'),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                      icon: const Icon(Icons.share),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    Builder(
 | 
				
			||||||
 | 
					                      builder: (context) => IconButton(
 | 
				
			||||||
 | 
					                        icon: const Icon(Icons.menu),
 | 
				
			||||||
 | 
					                        onPressed: () {
 | 
				
			||||||
 | 
					                          Scaffold.of(context).openEndDrawer();
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                SliverToBoxAdapter(
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      AnimatedBuilder(
 | 
				
			||||||
 | 
					                        animation: _pageController,
 | 
				
			||||||
 | 
					                        builder: (context, child) {
 | 
				
			||||||
 | 
					                          return buildAnimatedTransition(
 | 
				
			||||||
 | 
					                            context: context,
 | 
				
			||||||
 | 
					                            pageController: _pageController,
 | 
				
			||||||
 | 
					                            index: index,
 | 
				
			||||||
 | 
					                            controller: mediaController,
 | 
				
			||||||
 | 
					                            child: child!,
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                        child: _buildMedia(item, isActive),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      const SizedBox(height: 16),
 | 
				
			||||||
 | 
					                      Padding(
 | 
				
			||||||
 | 
					                        padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
				
			||||||
 | 
					                        child: Column(
 | 
				
			||||||
 | 
					                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Row(
 | 
				
			||||||
 | 
					                              mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                ...item.tags?.map(
 | 
				
			||||||
 | 
					                                  (tag) => Padding(
 | 
				
			||||||
 | 
					                                    padding: const EdgeInsets.only(right: 6),
 | 
				
			||||||
 | 
					                                    child: ActionTag(tag, (onTagTap) {
 | 
				
			||||||
 | 
					                                      if (tag.tag == 'sfw' || tag.tag == 'nsfw') {
 | 
				
			||||||
 | 
					                                        return;
 | 
				
			||||||
 | 
					                                      }
 | 
				
			||||||
 | 
					                                      mediaController.setTag(onTagTap);
 | 
				
			||||||
 | 
					                                      Get.offAllNamed('/');
 | 
				
			||||||
 | 
					                                    }),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ) ?? [],
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            if (authController.isLoggedIn) ...[
 | 
				
			||||||
 | 
					                              const SizedBox(height: 20),
 | 
				
			||||||
 | 
					                              Row(
 | 
				
			||||||
 | 
					                                children: [
 | 
				
			||||||
 | 
					                                  Expanded(
 | 
				
			||||||
 | 
					                                    child: SingleChildScrollView(
 | 
				
			||||||
 | 
					                                      scrollDirection: Axis.horizontal,
 | 
				
			||||||
 | 
					                                      child: FavoriteAvatars(
 | 
				
			||||||
 | 
					                                        favorites: item.favorites ?? [],
 | 
				
			||||||
 | 
					                                        brightness: Theme.of(
 | 
				
			||||||
 | 
					                                          context,
 | 
				
			||||||
 | 
					                                        ).brightness,
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                  IconButton(
 | 
				
			||||||
 | 
					                                    icon: isFavorite
 | 
				
			||||||
 | 
					                                        ? const Icon(Icons.favorite)
 | 
				
			||||||
 | 
					                                        : const Icon(Icons.favorite_outline),
 | 
				
			||||||
 | 
					                                    color: Colors.red,
 | 
				
			||||||
 | 
					                                    onPressed: () async {
 | 
				
			||||||
 | 
					                                      final List<Favorite>? newFavorites =
 | 
				
			||||||
 | 
					                                          await mediaController.toggleFavorite(
 | 
				
			||||||
 | 
					                                            item,
 | 
				
			||||||
 | 
					                                            isFavorite,
 | 
				
			||||||
 | 
					                                          );
 | 
				
			||||||
 | 
					                                      if (newFavorites != null) {
 | 
				
			||||||
 | 
					                                        mediaController.items[index] = item
 | 
				
			||||||
 | 
					                                            .copyWith(favorites: newFavorites);
 | 
				
			||||||
 | 
					                                        mediaController.items.refresh();
 | 
				
			||||||
 | 
					                                      }
 | 
				
			||||||
 | 
					                                      setState(() {});
 | 
				
			||||||
 | 
					                                    },
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ],
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                            ],
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            persistentFooterButtons: [
 | 
				
			||||||
 | 
					              Obx(() {
 | 
				
			||||||
 | 
					                if (mediaController.tag.value != null) {
 | 
				
			||||||
 | 
					                  return Center(
 | 
				
			||||||
 | 
					                    child: InputChip(
 | 
				
			||||||
 | 
					                      label: Text(mediaController.tag.value!),
 | 
				
			||||||
 | 
					                      onDeleted: () {
 | 
				
			||||||
 | 
					                        mediaController.setTag(null);
 | 
				
			||||||
 | 
					                        Get.offAllNamed('/');
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  return SizedBox.shrink();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,68 +1,75 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					import 'package:pullex/pullex.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
					import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
					import 'package:f0ckapp/widgets/end_drawer.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/widgets/filter_bar.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/screens/mediadetail.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/media_tile.dart';
 | 
					import 'package:f0ckapp/widgets/media_tile.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MediaGrid extends StatefulWidget {
 | 
					class MediaGrid extends StatefulWidget {
 | 
				
			||||||
  const MediaGrid({super.key});
 | 
					  const MediaGrid({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<MediaGrid> createState() => _MediaGridState();
 | 
					  State<MediaGrid> createState() => _MediaGrid();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _MediaGridState extends State<MediaGrid> {
 | 
					class _MediaGrid extends State<MediaGrid> {
 | 
				
			||||||
  final MediaController controller = Get.find<MediaController>();
 | 
					 | 
				
			||||||
  final ScrollController _scrollController = ScrollController();
 | 
					  final ScrollController _scrollController = ScrollController();
 | 
				
			||||||
  Timer? _debounce;
 | 
					  final MediaController _mediaController = Get.put(MediaController());
 | 
				
			||||||
 | 
					  final PullexRefreshController _refreshController = PullexRefreshController(
 | 
				
			||||||
 | 
					    initialRefresh: false,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    controller.loadMediaItems();
 | 
					    _mediaController.fetchInitial();
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ever(controller.drawerSwipeEnabled, (_) {
 | 
					 | 
				
			||||||
      setState(() {});
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _scrollController.addListener(() {
 | 
					 | 
				
			||||||
      if (_scrollController.position.extentAfter < 200 &&
 | 
					 | 
				
			||||||
          !controller.isLoading.value) {
 | 
					 | 
				
			||||||
        if (_debounce?.isActive ?? false) _debounce!.cancel();
 | 
					 | 
				
			||||||
        _debounce = Timer(const Duration(milliseconds: 300), () {
 | 
					 | 
				
			||||||
          controller.loadMediaItems(
 | 
					 | 
				
			||||||
            older: controller.mediaItems.isNotEmpty
 | 
					 | 
				
			||||||
                ? controller.mediaItems.last.id
 | 
					 | 
				
			||||||
                : null,
 | 
					 | 
				
			||||||
            append: true,
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      endDrawer: EndDrawer(),
 | 
					      endDrawer: EndDrawer(),
 | 
				
			||||||
      endDrawerEnableOpenDragGesture: controller.drawerSwipeEnabled.value,
 | 
					      endDrawerEnableOpenDragGesture: _mediaController.drawerSwipeEnabled.value,
 | 
				
			||||||
      body: RefreshIndicator(
 | 
					      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
				
			||||||
        edgeOffset: 100,
 | 
					      body: PullexRefresh(
 | 
				
			||||||
 | 
					        controller: _refreshController,
 | 
				
			||||||
 | 
					        enablePullDown: true,
 | 
				
			||||||
 | 
					        enablePullUp: true,
 | 
				
			||||||
 | 
					        header: MaterialHeader(offset: 140),
 | 
				
			||||||
        onRefresh: () async {
 | 
					        onRefresh: () async {
 | 
				
			||||||
          await controller.loadMediaItems();
 | 
					          try {
 | 
				
			||||||
 | 
					            if (_mediaController.loading.value) return;
 | 
				
			||||||
 | 
					            if (!_mediaController.atStart.value) {
 | 
				
			||||||
 | 
					              await _mediaController.fetchNewer();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              await _mediaController.fetchInitial();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } finally {
 | 
				
			||||||
 | 
					            _refreshController.refreshCompleted();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onLoading: () async {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            if (!_mediaController.loading.value &&
 | 
				
			||||||
 | 
					                !_mediaController.atEnd.value) {
 | 
				
			||||||
 | 
					              await _mediaController.fetchMore();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } finally {
 | 
				
			||||||
 | 
					            _refreshController.loadComplete();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        child: CustomScrollView(
 | 
					        child: CustomScrollView(
 | 
				
			||||||
          controller: _scrollController,
 | 
					          controller: _scrollController,
 | 
				
			||||||
          slivers: [
 | 
					          slivers: [
 | 
				
			||||||
            SliverAppBar(
 | 
					            SliverAppBar(
 | 
				
			||||||
              floating: true,
 | 
					              pinned: false,
 | 
				
			||||||
              snap: true,
 | 
					              snap: true,
 | 
				
			||||||
 | 
					              floating: true,
 | 
				
			||||||
              title: GestureDetector(
 | 
					              title: GestureDetector(
 | 
				
			||||||
                child: Row(
 | 
					                child: Row(
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
@@ -75,7 +82,7 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  controller.setTag(null);
 | 
					                  _mediaController.setTag(null);
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              actions: [
 | 
					              actions: [
 | 
				
			||||||
@@ -91,16 +98,15 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
                Obx(
 | 
					                Obx(
 | 
				
			||||||
                  () => IconButton(
 | 
					                  () => IconButton(
 | 
				
			||||||
                    icon: Icon(
 | 
					                    icon: Icon(
 | 
				
			||||||
                      controller.random.value
 | 
					                      _mediaController.random.value == 1
 | 
				
			||||||
                          ? Icons.shuffle_on_outlined
 | 
					                          ? Icons.shuffle_on_outlined
 | 
				
			||||||
                          : Icons.shuffle,
 | 
					                          : Icons.shuffle,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    onPressed: () async {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      await controller.toggleRandom();
 | 
					                      _mediaController.toggleRandom();
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					 | 
				
			||||||
                Builder(
 | 
					                Builder(
 | 
				
			||||||
                  builder: (context) {
 | 
					                  builder: (context) {
 | 
				
			||||||
                    return IconButton(
 | 
					                    return IconButton(
 | 
				
			||||||
@@ -113,14 +119,21 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            SliverPadding(
 | 
					            Obx(
 | 
				
			||||||
              padding: EdgeInsets.zero,
 | 
					              () => SliverPadding(
 | 
				
			||||||
              sliver: Obx(
 | 
					                padding: const EdgeInsets.all(4),
 | 
				
			||||||
                () => SliverGrid(
 | 
					                sliver: SliverGrid(
 | 
				
			||||||
                  delegate: SliverChildBuilderDelegate((context, index) {
 | 
					                  delegate: SliverChildBuilderDelegate((context, index) {
 | 
				
			||||||
                    return MediaTile(item: controller.mediaItems[index]);
 | 
					                    final item = _mediaController.filteredItems[index];
 | 
				
			||||||
                  }, childCount: controller.mediaItems.length),
 | 
					                    return GestureDetector(
 | 
				
			||||||
                  gridDelegate: controller.crossAxisCount.value == 0
 | 
					                      onTap: () {
 | 
				
			||||||
 | 
					                        final item = _mediaController.filteredItems[index];
 | 
				
			||||||
 | 
					                        Get.to(() => MediaDetailScreen(initialId: item.id));
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: MediaTile(item: item),
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  }, childCount: _mediaController.filteredItems.length),
 | 
				
			||||||
 | 
					                  gridDelegate: _mediaController.crossAxisCount.value == 0
 | 
				
			||||||
                      ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
					                      ? const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
				
			||||||
                          maxCrossAxisExtent: 150,
 | 
					                          maxCrossAxisExtent: 150,
 | 
				
			||||||
                          crossAxisSpacing: 5,
 | 
					                          crossAxisSpacing: 5,
 | 
				
			||||||
@@ -128,7 +141,7 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
                          childAspectRatio: 1,
 | 
					                          childAspectRatio: 1,
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                      : SliverGridDelegateWithFixedCrossAxisCount(
 | 
					                      : SliverGridDelegateWithFixedCrossAxisCount(
 | 
				
			||||||
                          crossAxisCount: controller.crossAxisCount.value,
 | 
					                          crossAxisCount: _mediaController.crossAxisCount.value,
 | 
				
			||||||
                          crossAxisSpacing: 5,
 | 
					                          crossAxisSpacing: 5,
 | 
				
			||||||
                          mainAxisSpacing: 5,
 | 
					                          mainAxisSpacing: 5,
 | 
				
			||||||
                          childAspectRatio: 1,
 | 
					                          childAspectRatio: 1,
 | 
				
			||||||
@@ -141,12 +154,12 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
      ),
 | 
					      ),
 | 
				
			||||||
      persistentFooterButtons: [
 | 
					      persistentFooterButtons: [
 | 
				
			||||||
        Obx(() {
 | 
					        Obx(() {
 | 
				
			||||||
          if (controller.tag.value != null) {
 | 
					          if (_mediaController.tag.value != null) {
 | 
				
			||||||
            return Center(
 | 
					            return Center(
 | 
				
			||||||
              child: InputChip(
 | 
					              child: InputChip(
 | 
				
			||||||
                label: Text(controller.tag.value!),
 | 
					                label: Text(_mediaController.tag.value!),
 | 
				
			||||||
                onDeleted: () async {
 | 
					                onDeleted: () {
 | 
				
			||||||
                  await controller.setTag(null);
 | 
					                  _mediaController.setTag(null);
 | 
				
			||||||
                  Get.offAllNamed('/');
 | 
					                  Get.offAllNamed('/');
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -156,7 +169,6 @@ class _MediaGridState extends State<MediaGrid> {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -3,8 +3,8 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
					import 'package:flutter_cache_manager/flutter_cache_manager.dart';
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/localization_controller.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
					import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SettingsPage extends StatefulWidget {
 | 
					class SettingsPage extends StatefulWidget {
 | 
				
			||||||
@@ -1,107 +0,0 @@
 | 
				
			|||||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
					 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
 | 
					 | 
				
			||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class MediaService extends GetConnect {
 | 
					 | 
				
			||||||
  final EncryptedSharedPreferencesAsync storage =
 | 
					 | 
				
			||||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<List<MediaItem>> fetchMediaItems({
 | 
					 | 
				
			||||||
    required int type,
 | 
					 | 
				
			||||||
    required int mode,
 | 
					 | 
				
			||||||
    required int random,
 | 
					 | 
				
			||||||
    String? tag,
 | 
					 | 
				
			||||||
    int? older,
 | 
					 | 
				
			||||||
  }) async {
 | 
					 | 
				
			||||||
    final String? token = await storage.getString('token');
 | 
					 | 
				
			||||||
    final Map<String, String> headers = token != null
 | 
					 | 
				
			||||||
        ? {'Authorization': 'Bearer $token'}
 | 
					 | 
				
			||||||
        : {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final queryParameters = {
 | 
					 | 
				
			||||||
      'type': type.toString(),
 | 
					 | 
				
			||||||
      'mode': mode.toString(),
 | 
					 | 
				
			||||||
      'random': random.toString(),
 | 
					 | 
				
			||||||
      if (tag != null) 'tag': tag,
 | 
					 | 
				
			||||||
      if (older != null) 'older': older.toString(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final Response<dynamic> response = await get(
 | 
					 | 
				
			||||||
        'https://api.f0ck.me/items/get',
 | 
					 | 
				
			||||||
        query: queryParameters,
 | 
					 | 
				
			||||||
        headers: headers,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (response.status.code == 200 && response.body is List) {
 | 
					 | 
				
			||||||
        final data = response.body as List<dynamic>;
 | 
					 | 
				
			||||||
        return data.map((json) => MediaItem.fromJson(json)).toList();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return Future.error('Fehler beim Laden der Daten: ${response.body}');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      return Future.error('Netzwerkfehler: ${e.toString()}');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<List<Favorite>?> toggleFavorite(
 | 
					 | 
				
			||||||
    MediaItem item,
 | 
					 | 
				
			||||||
    bool isFavorite,
 | 
					 | 
				
			||||||
  ) async {
 | 
					 | 
				
			||||||
    final String? token = await storage.getString('token');
 | 
					 | 
				
			||||||
    if (token == null) return null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final headers = {
 | 
					 | 
				
			||||||
      'Authorization': 'Bearer $token',
 | 
					 | 
				
			||||||
      'Content-Type': 'application/json',
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      Response response;
 | 
					 | 
				
			||||||
      if (!isFavorite) {
 | 
					 | 
				
			||||||
        response = await put(
 | 
					 | 
				
			||||||
          'https://api.f0ck.me/favorites/${item.id}',
 | 
					 | 
				
			||||||
          null,
 | 
					 | 
				
			||||||
          headers: headers,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        response = await delete(
 | 
					 | 
				
			||||||
          'https://api.f0ck.me/favorites/${item.id}',
 | 
					 | 
				
			||||||
          headers: headers,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (response.status.code == 200 && response.body is List) {
 | 
					 | 
				
			||||||
        return (response.body as List)
 | 
					 | 
				
			||||||
            .map((json) => Favorite.fromJson(json))
 | 
					 | 
				
			||||||
            .toList();
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return null;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<MediaItem?> fetchItem(int itemId) async {
 | 
					 | 
				
			||||||
    final String? token = await storage.getString('token');
 | 
					 | 
				
			||||||
    final Map<String, String> headers = token != null
 | 
					 | 
				
			||||||
        ? {'Authorization': 'Bearer $token'}
 | 
					 | 
				
			||||||
        : {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      final Response<dynamic> response = await get(
 | 
					 | 
				
			||||||
        'https://api.f0ck.me/item/$itemId',
 | 
					 | 
				
			||||||
        headers: headers,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (response.status.code == 200 && response.body is Map) {
 | 
					 | 
				
			||||||
        return MediaItem.fromJson(response.body);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										86
									
								
								lib/services/api.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/services/api.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					import 'package:encrypt_shared_preferences/provider.dart';
 | 
				
			||||||
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/feed.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ApiService extends GetConnect {
 | 
				
			||||||
 | 
					  final EncryptedSharedPreferencesAsync storage =
 | 
				
			||||||
 | 
					      EncryptedSharedPreferencesAsync.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Feed> fetchItems({
 | 
				
			||||||
 | 
					    int? older,
 | 
				
			||||||
 | 
					    int? newer,
 | 
				
			||||||
 | 
					    int type = 0,
 | 
				
			||||||
 | 
					    int mode = 0,
 | 
				
			||||||
 | 
					    int random = 0,
 | 
				
			||||||
 | 
					    String? tag,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    String? token = await storage.getString('token');
 | 
				
			||||||
 | 
					    final params = <String, String>{
 | 
				
			||||||
 | 
					      'type': type.toString(),
 | 
				
			||||||
 | 
					      'mode': mode.toString(),
 | 
				
			||||||
 | 
					      'random': random.toString(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    if (older != null) params['older'] = older.toString();
 | 
				
			||||||
 | 
					    if (newer != null) params['newer'] = newer.toString();
 | 
				
			||||||
 | 
					    if (tag != null) params['tag'] = tag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Map<String, String> headers = <String, String>{};
 | 
				
			||||||
 | 
					    if (token != null && token.isNotEmpty) {
 | 
				
			||||||
 | 
					      headers['Authorization'] = 'Bearer $token';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Response<dynamic> response = await get(
 | 
				
			||||||
 | 
					      'https://api.f0ck.me/items_new/get',
 | 
				
			||||||
 | 
					      query: params,
 | 
				
			||||||
 | 
					      headers: headers,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (response.status.code == 200 && response.body is Map<String, dynamic>) {
 | 
				
			||||||
 | 
					      final Feed feed = Feed.fromJson(response.body as Map<String, dynamic>);
 | 
				
			||||||
 | 
					      feed.items.sort((a, b) => b.id.compareTo(a.id));
 | 
				
			||||||
 | 
					      return feed;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      throw Exception('Fehler beim Laden der Items');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<Favorite>?> toggleFavorite(
 | 
				
			||||||
 | 
					    MediaItem item,
 | 
				
			||||||
 | 
					    bool isFavorite,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    String? token = await storage.getString('token');
 | 
				
			||||||
 | 
					    if (token == null || token.isEmpty) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Map<String, String> headers = {
 | 
				
			||||||
 | 
					      'Authorization': 'Bearer $token',
 | 
				
			||||||
 | 
					      'Content-Type': 'application/json',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      Response response;
 | 
				
			||||||
 | 
					      if (!isFavorite) {
 | 
				
			||||||
 | 
					        response = await put(
 | 
				
			||||||
 | 
					          'https://api.f0ck.me/favorites/${item.id}',
 | 
				
			||||||
 | 
					          null,
 | 
				
			||||||
 | 
					          headers: headers,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        response = await delete(
 | 
				
			||||||
 | 
					          'https://api.f0ck.me/favorites/${item.id}',
 | 
				
			||||||
 | 
					          headers: headers,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (response.status.code == 200 && response.body is List) {
 | 
				
			||||||
 | 
					        return (response.body as List)
 | 
				
			||||||
 | 
					            .map((json) => Favorite.fromJson(json))
 | 
				
			||||||
 | 
					            .toList();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,50 +1,50 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum PageTransition { opacity, scale, slide, rotate, flip }
 | 
					enum PageTransition { opacity, scale, slide, rotate, flip }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Widget buildAnimatedTransition({
 | 
					Widget buildAnimatedTransition({
 | 
				
			||||||
    required BuildContext context,
 | 
					  required BuildContext context,
 | 
				
			||||||
    required Widget child,
 | 
					  required Widget child,
 | 
				
			||||||
    required PageController pageController,
 | 
					  required PageController pageController,
 | 
				
			||||||
    required int index,
 | 
					  required int index,
 | 
				
			||||||
    required MediaController controller,
 | 
					  required MediaController controller,
 | 
				
			||||||
  }) {
 | 
					}) {
 | 
				
			||||||
    final double value = pageController.position.haveDimensions
 | 
					  final double value = pageController.position.haveDimensions
 | 
				
			||||||
        ? pageController.page! - index
 | 
					      ? pageController.page! - index
 | 
				
			||||||
        : 0;
 | 
					      : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (controller.transitionType.value) {
 | 
					  switch (controller.transitionType.value) {
 | 
				
			||||||
      case PageTransition.opacity:
 | 
					    case PageTransition.opacity:
 | 
				
			||||||
        return Opacity(
 | 
					      return Opacity(
 | 
				
			||||||
          opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)),
 | 
					        opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)),
 | 
				
			||||||
          child: Transform(transform: Matrix4.identity(), child: child),
 | 
					        child: Transform(transform: Matrix4.identity(), child: child),
 | 
				
			||||||
        );
 | 
					      );
 | 
				
			||||||
      case PageTransition.scale:
 | 
					    case PageTransition.scale:
 | 
				
			||||||
        return Transform.scale(
 | 
					      return Transform.scale(
 | 
				
			||||||
          scale:
 | 
					        scale:
 | 
				
			||||||
              0.8 +
 | 
					            0.8 +
 | 
				
			||||||
              Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)) * 0.2,
 | 
					            Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)) * 0.2,
 | 
				
			||||||
          child: child,
 | 
					        child: child,
 | 
				
			||||||
        );
 | 
					      );
 | 
				
			||||||
      case PageTransition.slide:
 | 
					    case PageTransition.slide:
 | 
				
			||||||
        return Transform.translate(
 | 
					      return Transform.translate(
 | 
				
			||||||
          offset: Offset(300 * value.abs(), 0),
 | 
					        offset: Offset(300 * value.abs(), 0),
 | 
				
			||||||
          child: child,
 | 
					        child: child,
 | 
				
			||||||
        );
 | 
					      );
 | 
				
			||||||
      case PageTransition.rotate:
 | 
					    case PageTransition.rotate:
 | 
				
			||||||
        return Opacity(
 | 
					      return Opacity(
 | 
				
			||||||
          opacity: (1 - value.abs()).clamp(0.0, 1.0),
 | 
					        opacity: (1 - value.abs()).clamp(0.0, 1.0),
 | 
				
			||||||
          child: Transform.rotate(angle: value.abs() * 0.5, child: child),
 | 
					        child: Transform.rotate(angle: value.abs() * 0.5, child: child),
 | 
				
			||||||
        );
 | 
					      );
 | 
				
			||||||
      case PageTransition.flip:
 | 
					    case PageTransition.flip:
 | 
				
			||||||
        return Transform(
 | 
					      return Transform(
 | 
				
			||||||
          transform: Matrix4.identity()
 | 
					        transform: Matrix4.identity()
 | 
				
			||||||
            ..setEntry(3, 2, 0.001)
 | 
					          ..setEntry(3, 2, 0.001)
 | 
				
			||||||
            ..rotateY(value.abs()),
 | 
					          ..rotateY(value.abs()),
 | 
				
			||||||
          alignment: Alignment.center,
 | 
					        alignment: Alignment.center,
 | 
				
			||||||
          child: child,
 | 
					        child: child,
 | 
				
			||||||
        );
 | 
					      );
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:convert';
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/suggestion_model.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/suggestion.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CustomSearchDelegate extends SearchDelegate<String> {
 | 
					class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			||||||
  final MediaController controller = Get.find<MediaController>();
 | 
					  final MediaController controller = Get.find<MediaController>();
 | 
				
			||||||
@@ -144,8 +144,8 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
				
			|||||||
                'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
 | 
					                'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
 | 
				
			||||||
                style: TextStyle(fontSize: 12),
 | 
					                style: TextStyle(fontSize: 12),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              onTap: () async {
 | 
					              onTap: () {
 | 
				
			||||||
                await controller.setTag(suggestion.tag);
 | 
					                controller.setTag(suggestion.tag);
 | 
				
			||||||
                close(context, suggestion.tag);
 | 
					                close(context, suggestion.tag);
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActionTag extends StatelessWidget {
 | 
					class ActionTag extends StatelessWidget {
 | 
				
			||||||
  final Tag tag;
 | 
					  final Tag tag;
 | 
				
			||||||
@@ -13,13 +13,7 @@ class ActionTag extends StatelessWidget {
 | 
				
			|||||||
    return GestureDetector(
 | 
					    return GestureDetector(
 | 
				
			||||||
      onTap: () => onTagTap(tag.tag),
 | 
					      onTap: () => onTagTap(tag.tag),
 | 
				
			||||||
      child:
 | 
					      child:
 | 
				
			||||||
          [
 | 
					          ['german', 'dutch', 'ukraine', 'russia', 'belgium'].contains(tag.tag)
 | 
				
			||||||
            'german',
 | 
					 | 
				
			||||||
            'dutch',
 | 
					 | 
				
			||||||
            'ukraine',
 | 
					 | 
				
			||||||
            'russia',
 | 
					 | 
				
			||||||
            'belgium',
 | 
					 | 
				
			||||||
          ].contains(tag.tag)
 | 
					 | 
				
			||||||
          ? Stack(
 | 
					          ? Stack(
 | 
				
			||||||
              alignment: Alignment.center,
 | 
					              alignment: Alignment.center,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,121 +0,0 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/service/media_service.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/actiontag.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/favorite_avatars.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
					 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DetailMediaContent extends StatelessWidget {
 | 
					 | 
				
			||||||
  final int currentPage;
 | 
					 | 
				
			||||||
  final int index;
 | 
					 | 
				
			||||||
  final void Function(String tag) onTagTap;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const DetailMediaContent({
 | 
					 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.currentPage,
 | 
					 | 
				
			||||||
    required this.index,
 | 
					 | 
				
			||||||
    required this.onTagTap,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					 | 
				
			||||||
    final MediaService mediaService = Get.find();
 | 
					 | 
				
			||||||
    final MediaController controller = Get.find();
 | 
					 | 
				
			||||||
    final AuthController authController = Get.find();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return SafeArea(
 | 
					 | 
				
			||||||
      top: false,
 | 
					 | 
				
			||||||
      child: SingleChildScrollView(
 | 
					 | 
				
			||||||
        child: Obx(() {
 | 
					 | 
				
			||||||
          final MediaItem currentItem = controller.mediaItems[index];
 | 
					 | 
				
			||||||
          final bool isFavorite =
 | 
					 | 
				
			||||||
              currentItem.favorites?.any(
 | 
					 | 
				
			||||||
                (f) => f.userId == authController.userId.value,
 | 
					 | 
				
			||||||
              ) ??
 | 
					 | 
				
			||||||
              false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return Column(
 | 
					 | 
				
			||||||
            children: [
 | 
					 | 
				
			||||||
              _buildMedia(currentItem, index == currentPage),
 | 
					 | 
				
			||||||
              const SizedBox(height: 10, width: double.infinity),
 | 
					 | 
				
			||||||
              _buildTags(currentItem),
 | 
					 | 
				
			||||||
              if (currentItem.favorites != null &&
 | 
					 | 
				
			||||||
                  authController.token.value != null) ...[
 | 
					 | 
				
			||||||
                const SizedBox(height: 20),
 | 
					 | 
				
			||||||
                _buildFavoritesRow(context, currentItem, isFavorite, () async {
 | 
					 | 
				
			||||||
                  final List<Favorite>? newFavorites = await mediaService
 | 
					 | 
				
			||||||
                      .toggleFavorite(currentItem, isFavorite);
 | 
					 | 
				
			||||||
                  if (newFavorites != null) {
 | 
					 | 
				
			||||||
                    controller.mediaItems[index] = currentItem.copyWith(
 | 
					 | 
				
			||||||
                      favorites: newFavorites,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                    controller.mediaItems.refresh();
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
              const SizedBox(height: 20),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Widget _buildMedia(MediaItem item, bool isActive) {
 | 
					 | 
				
			||||||
    if (item.mime.startsWith('image')) {
 | 
					 | 
				
			||||||
      return CachedNetworkImage(
 | 
					 | 
				
			||||||
        imageUrl: item.mediaUrl,
 | 
					 | 
				
			||||||
        fit: BoxFit.contain,
 | 
					 | 
				
			||||||
        placeholder: (context, url) =>
 | 
					 | 
				
			||||||
            const Center(child: CircularProgressIndicator()),
 | 
					 | 
				
			||||||
        errorWidget: (context, url, error) =>
 | 
					 | 
				
			||||||
            const Center(child: Icon(Icons.error)),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return VideoWidget(details: item, isActive: isActive);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Widget _buildTags(MediaItem item) {
 | 
					 | 
				
			||||||
    return Wrap(
 | 
					 | 
				
			||||||
      alignment: WrapAlignment.center,
 | 
					 | 
				
			||||||
      spacing: 5.0,
 | 
					 | 
				
			||||||
      children: item.tags
 | 
					 | 
				
			||||||
          .map<Widget>((Tag tag) => ActionTag(tag, onTagTap))
 | 
					 | 
				
			||||||
          .toList(),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Widget _buildFavoritesRow(
 | 
					 | 
				
			||||||
    BuildContext context,
 | 
					 | 
				
			||||||
    MediaItem item,
 | 
					 | 
				
			||||||
    bool isFavorite,
 | 
					 | 
				
			||||||
    VoidCallback onFavoritePressed,
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    return Row(
 | 
					 | 
				
			||||||
      children: [
 | 
					 | 
				
			||||||
        Expanded(
 | 
					 | 
				
			||||||
          child: SingleChildScrollView(
 | 
					 | 
				
			||||||
            scrollDirection: Axis.horizontal,
 | 
					 | 
				
			||||||
            child: FavoriteAvatars(
 | 
					 | 
				
			||||||
              favorites: item.favorites ?? [],
 | 
					 | 
				
			||||||
              brightness: Theme.of(context).brightness,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        IconButton(
 | 
					 | 
				
			||||||
          icon: isFavorite
 | 
					 | 
				
			||||||
              ? const Icon(Icons.favorite)
 | 
					 | 
				
			||||||
              : const Icon(Icons.favorite_outline),
 | 
					 | 
				
			||||||
          color: Colors.red,
 | 
					 | 
				
			||||||
          onPressed: onFavoritePressed,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/screens/login_screen.dart';
 | 
					import 'package:f0ckapp/controller/authcontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
					import 'package:f0ckapp/controller/themecontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/screens/settings_screen.dart';
 | 
					import 'package:f0ckapp/screens/login.dart';
 | 
				
			||||||
import 'package:f0ckapp/controller/theme_controller.dart';
 | 
					import 'package:f0ckapp/screens/settings.dart';
 | 
				
			||||||
import 'package:f0ckapp/utils/appversion.dart';
 | 
					import 'package:f0ckapp/utils/appversion.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EndDrawer extends StatelessWidget {
 | 
					class EndDrawer extends StatelessWidget {
 | 
				
			||||||
@@ -89,7 +89,7 @@ class EndDrawer extends StatelessWidget {
 | 
				
			|||||||
                    ElevatedButton(
 | 
					                    ElevatedButton(
 | 
				
			||||||
                      onPressed: () {
 | 
					                      onPressed: () {
 | 
				
			||||||
                        Navigator.pop(context);
 | 
					                        Navigator.pop(context);
 | 
				
			||||||
                        Get.to(() => LoginPage());
 | 
					                        Get.to(() => LoginScreen());
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                      child: const Text('Login'),
 | 
					                      child: const Text('Login'),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FavoriteAvatars extends StatelessWidget {
 | 
					class FavoriteAvatars extends StatelessWidget {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,16 +2,12 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
import 'package:f0ckapp/service/media_service.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FilterBar extends StatelessWidget {
 | 
					class FilterBar extends StatelessWidget {
 | 
				
			||||||
  final ScrollController scrollController;
 | 
					  final ScrollController scrollController;
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  const FilterBar({
 | 
					  const FilterBar({super.key, required this.scrollController});
 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.scrollController,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
@@ -23,39 +19,43 @@ class FilterBar extends StatelessWidget {
 | 
				
			|||||||
        mainAxisAlignment: MainAxisAlignment.spaceAround,
 | 
					        mainAxisAlignment: MainAxisAlignment.spaceAround,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          const Text('type: '),
 | 
					          const Text('type: '),
 | 
				
			||||||
          Obx(() => DropdownButton<String>(
 | 
					          Obx(
 | 
				
			||||||
            value: mediaTypes[c.type.value],
 | 
					            () => DropdownButton<String>(
 | 
				
			||||||
            isDense: true,
 | 
					              value: mediaTypes[c.typeIndex.value],
 | 
				
			||||||
            items: mediaTypes.map((String value) {
 | 
					              isDense: true,
 | 
				
			||||||
              return DropdownMenuItem<String>(
 | 
					              items: mediaTypes.map((String value) {
 | 
				
			||||||
                value: value,
 | 
					                return DropdownMenuItem<String>(
 | 
				
			||||||
                child: Text(value),
 | 
					                  value: value,
 | 
				
			||||||
              );
 | 
					                  child: Text(value),
 | 
				
			||||||
            }).toList(),
 | 
					                );
 | 
				
			||||||
            onChanged: (String? newValue) {
 | 
					              }).toList(),
 | 
				
			||||||
              if (newValue != null) {
 | 
					              onChanged: (String? newValue) {
 | 
				
			||||||
                c.setType(mediaTypes.indexOf(newValue));
 | 
					                if (newValue != null) {
 | 
				
			||||||
                scrollController.jumpTo(0);
 | 
					                  c.setTypeIndex(mediaTypes.indexOf(newValue));
 | 
				
			||||||
              }
 | 
					                  scrollController.jumpTo(0);
 | 
				
			||||||
            },
 | 
					                }
 | 
				
			||||||
          )),
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
          const Text('mode: '),
 | 
					          const Text('mode: '),
 | 
				
			||||||
          Obx(() => DropdownButton<String>(
 | 
					          Obx(
 | 
				
			||||||
            value: mediaModes[c.mode.value],
 | 
					            () => DropdownButton<String>(
 | 
				
			||||||
            isDense: true,
 | 
					              value: mediaModes[c.modeIndex.value],
 | 
				
			||||||
            items: mediaModes.map((String value) {
 | 
					              isDense: true,
 | 
				
			||||||
              return DropdownMenuItem<String>(
 | 
					              items: mediaModes.map((String value) {
 | 
				
			||||||
                value: value,
 | 
					                return DropdownMenuItem<String>(
 | 
				
			||||||
                child: Text(value),
 | 
					                  value: value,
 | 
				
			||||||
              );
 | 
					                  child: Text(value),
 | 
				
			||||||
            }).toList(),
 | 
					                );
 | 
				
			||||||
            onChanged: (String? newValue) {
 | 
					              }).toList(),
 | 
				
			||||||
              if (newValue != null) {
 | 
					              onChanged: (String? newValue) {
 | 
				
			||||||
                c.setMode(mediaModes.indexOf(newValue));
 | 
					                if (newValue != null) {
 | 
				
			||||||
                scrollController.jumpTo(0);
 | 
					                  c.setModeIndex(mediaModes.indexOf(newValue));
 | 
				
			||||||
              }
 | 
					                  scrollController.jumpTo(0);
 | 
				
			||||||
            },
 | 
					                }
 | 
				
			||||||
          )),
 | 
					              },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,11 +3,10 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MediaTile extends StatelessWidget {
 | 
					class MediaTile extends StatelessWidget {
 | 
				
			||||||
  final MediaItem item;
 | 
					  final MediaItem item;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  const MediaTile({super.key, required this.item});
 | 
					  const MediaTile({super.key, required this.item});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -20,14 +19,12 @@ class MediaTile extends StatelessWidget {
 | 
				
			|||||||
        child: Stack(
 | 
					        child: Stack(
 | 
				
			||||||
          fit: StackFit.expand,
 | 
					          fit: StackFit.expand,
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            Hero(
 | 
					            CachedNetworkImage(
 | 
				
			||||||
              tag: 'media-${item.id}',
 | 
					              imageUrl: 'https://f0ck.me/t/${item.id}.webp',
 | 
				
			||||||
              child: CachedNetworkImage(
 | 
					              fit: BoxFit.cover,
 | 
				
			||||||
                imageUrl: item.thumbnailUrl,
 | 
					              placeholder: (context, url) => Container(color: Colors.grey[900]),
 | 
				
			||||||
                fit: BoxFit.cover,
 | 
					              errorWidget: (context, url, error) =>
 | 
				
			||||||
                placeholder: (content, url) => Container(color: Colors.grey[900]),
 | 
					                  const Icon(Icons.broken_image),
 | 
				
			||||||
                errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Align(
 | 
					            Align(
 | 
				
			||||||
              alignment: Alignment.bottomRight,
 | 
					              alignment: Alignment.bottomRight,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,75 +1,66 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoControlsOverlay extends StatelessWidget {
 | 
					class VideoControlsOverlay extends StatelessWidget {
 | 
				
			||||||
  final CachedVideoPlayerPlusController controller;
 | 
					  final CachedVideoPlayerPlusController controller;
 | 
				
			||||||
  final VoidCallback button;
 | 
					  final VoidCallback onOverlayTap;
 | 
				
			||||||
 | 
					  final bool muted;
 | 
				
			||||||
 | 
					  final VoidCallback onMuteToggle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const VideoControlsOverlay({
 | 
					  const VideoControlsOverlay({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.controller,
 | 
					    required this.controller,
 | 
				
			||||||
    required this.button,
 | 
					    required this.onOverlayTap,
 | 
				
			||||||
 | 
					    required this.muted,
 | 
				
			||||||
 | 
					    required this.onMuteToggle,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final MediaController c = Get.find<MediaController>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      alignment: Alignment.center,
 | 
					      alignment: Alignment.center,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
 | 
					        Row(
 | 
				
			||||||
 | 
					          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            _ControlButton(Icons.replay_10, () {
 | 
				
			||||||
 | 
					              onOverlayTap();
 | 
				
			||||||
 | 
					              Duration newPosition =
 | 
				
			||||||
 | 
					                  controller.value.position - const Duration(seconds: 10);
 | 
				
			||||||
 | 
					              if (newPosition < Duration.zero) newPosition = Duration.zero;
 | 
				
			||||||
 | 
					              controller.seekTo(newPosition);
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            const SizedBox(width: 40),
 | 
				
			||||||
 | 
					            _ControlButton(
 | 
				
			||||||
 | 
					              controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
				
			||||||
 | 
					              () {
 | 
				
			||||||
 | 
					                onOverlayTap();
 | 
				
			||||||
 | 
					                controller.value.isPlaying
 | 
				
			||||||
 | 
					                    ? controller.pause()
 | 
				
			||||||
 | 
					                    : controller.play();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              size: 64,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            const SizedBox(width: 40),
 | 
				
			||||||
 | 
					            _ControlButton(Icons.forward_10, () {
 | 
				
			||||||
 | 
					              onOverlayTap();
 | 
				
			||||||
 | 
					              Duration newPosition =
 | 
				
			||||||
 | 
					                  controller.value.position + const Duration(seconds: 10);
 | 
				
			||||||
 | 
					              if (newPosition > controller.value.duration) {
 | 
				
			||||||
 | 
					                newPosition = controller.value.duration;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              controller.seekTo(newPosition);
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        Positioned(
 | 
					        Positioned(
 | 
				
			||||||
          right: 12,
 | 
					          right: 12,
 | 
				
			||||||
          bottom: 12,
 | 
					          bottom: 12,
 | 
				
			||||||
          child: Obx(
 | 
					          child: _ControlButton(muted ? Icons.volume_off : Icons.volume_up, () {
 | 
				
			||||||
            () => _ControlButton(
 | 
					            onOverlayTap();
 | 
				
			||||||
              c.muted.value ? Icons.volume_off : Icons.volume_up,
 | 
					            onMuteToggle();
 | 
				
			||||||
              () async {
 | 
					          }, size: 16),
 | 
				
			||||||
                button();
 | 
					 | 
				
			||||||
                await c.toggleMuted();
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              size: 16,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        Center(
 | 
					 | 
				
			||||||
          child: Row(
 | 
					 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
					 | 
				
			||||||
            children: [
 | 
					 | 
				
			||||||
              _ControlButton(Icons.replay_10, () {
 | 
					 | 
				
			||||||
                button();
 | 
					 | 
				
			||||||
                Duration newPosition =
 | 
					 | 
				
			||||||
                    controller.value.position - const Duration(seconds: 10);
 | 
					 | 
				
			||||||
                if (newPosition < Duration.zero) newPosition = Duration.zero;
 | 
					 | 
				
			||||||
                controller.seekTo(newPosition);
 | 
					 | 
				
			||||||
              }),
 | 
					 | 
				
			||||||
              SizedBox(width: 40),
 | 
					 | 
				
			||||||
              _ControlButton(
 | 
					 | 
				
			||||||
                controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
					 | 
				
			||||||
                () {
 | 
					 | 
				
			||||||
                  button();
 | 
					 | 
				
			||||||
                  controller.value.isPlaying
 | 
					 | 
				
			||||||
                      ? controller.pause()
 | 
					 | 
				
			||||||
                      : controller.play();
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                size: 64,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              SizedBox(width: 40),
 | 
					 | 
				
			||||||
              _ControlButton(Icons.forward_10, () {
 | 
					 | 
				
			||||||
                button();
 | 
					 | 
				
			||||||
                Duration newPosition =
 | 
					 | 
				
			||||||
                    controller.value.position + const Duration(seconds: 10);
 | 
					 | 
				
			||||||
                if (newPosition > controller.value.duration) {
 | 
					 | 
				
			||||||
                  newPosition = controller.value.duration;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                controller.seekTo(newPosition);
 | 
					 | 
				
			||||||
              }),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        Align(
 | 
					        Align(
 | 
				
			||||||
          alignment: Alignment.bottomCenter,
 | 
					          alignment: Alignment.bottomCenter,
 | 
				
			||||||
@@ -83,11 +74,12 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
                  bottom: 12,
 | 
					                  bottom: 12,
 | 
				
			||||||
                  child: Text(
 | 
					                  child: Text(
 | 
				
			||||||
                    '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
 | 
					                    '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
 | 
				
			||||||
 | 
					                    style: const TextStyle(color: Colors.white, fontSize: 12),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                Listener(
 | 
					                Listener(
 | 
				
			||||||
                  onPointerDown: (_) {
 | 
					                  onPointerDown: (_) {
 | 
				
			||||||
                    button();
 | 
					                    onOverlayTap();
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  child: VideoProgressIndicator(
 | 
					                  child: VideoProgressIndicator(
 | 
				
			||||||
                    controller,
 | 
					                    controller,
 | 
				
			||||||
@@ -100,23 +92,24 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                Positioned(
 | 
					                if (controller.value.duration.inMilliseconds > 0)
 | 
				
			||||||
                  left:
 | 
					                  Positioned(
 | 
				
			||||||
                      (controller.value.position.inMilliseconds /
 | 
					                    left:
 | 
				
			||||||
                              controller.value.duration.inMilliseconds) *
 | 
					                        (controller.value.position.inMilliseconds /
 | 
				
			||||||
                          MediaQuery.of(context).size.width -
 | 
					                                controller.value.duration.inMilliseconds) *
 | 
				
			||||||
                      6,
 | 
					                            MediaQuery.of(context).size.width -
 | 
				
			||||||
                  bottom: -4,
 | 
					                        6,
 | 
				
			||||||
                  child: Container(
 | 
					                    bottom: -4,
 | 
				
			||||||
                    width: 12,
 | 
					                    child: Container(
 | 
				
			||||||
                    height: 12,
 | 
					                      width: 12,
 | 
				
			||||||
                    decoration: BoxDecoration(
 | 
					                      height: 12,
 | 
				
			||||||
                      shape: BoxShape.circle,
 | 
					                      decoration: BoxDecoration(
 | 
				
			||||||
                      color: Colors.red,
 | 
					                        shape: BoxShape.circle,
 | 
				
			||||||
                      border: Border.all(color: Colors.red, width: 2),
 | 
					                        color: Colors.red,
 | 
				
			||||||
 | 
					                        border: Border.all(color: Colors.red, width: 2),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:get/get.dart';
 | 
					import 'package:get/get.dart';
 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					 | 
				
			||||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
					import 'package:f0ckapp/models/item.dart';
 | 
				
			||||||
import 'package:f0ckapp/models/media_item.dart';
 | 
					import 'package:f0ckapp/widgets/video_controls_overlay.dart';
 | 
				
			||||||
import 'package:f0ckapp/widgets/videooverlay_widget.dart';
 | 
					import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoWidget extends StatefulWidget {
 | 
					class VideoWidget extends StatefulWidget {
 | 
				
			||||||
  final MediaItem details;
 | 
					  final MediaItem details;
 | 
				
			||||||
@@ -44,15 +44,13 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    await _controller.initialize();
 | 
					    await _controller.initialize();
 | 
				
			||||||
    setState(() {});
 | 
					    setState(() {});
 | 
				
			||||||
 | 
					 | 
				
			||||||
    _controller.addListener(() => setState(() {}));
 | 
					    _controller.addListener(() => setState(() {}));
 | 
				
			||||||
 | 
					    _controller.setLooping(true);
 | 
				
			||||||
 | 
					    _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.isActive) {
 | 
					    if (widget.isActive) {
 | 
				
			||||||
      _controller.play();
 | 
					      _controller.play();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    _controller.setLooping(true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -96,34 +94,31 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
					    bool isAudio = widget.details.mime.startsWith('audio');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (widget.fullScreen) {
 | 
					    Widget mediaContent;
 | 
				
			||||||
      return Stack(
 | 
					    if (isAudio) {
 | 
				
			||||||
 | 
					      mediaContent = CachedNetworkImage(
 | 
				
			||||||
 | 
					        imageUrl: widget.details.coverUrl,
 | 
				
			||||||
 | 
					        fit: BoxFit.cover,
 | 
				
			||||||
 | 
					        errorWidget: (c, e, s) => Image.asset(
 | 
				
			||||||
 | 
					          'assets/images/music.webp',
 | 
				
			||||||
 | 
					          fit: BoxFit.contain,
 | 
				
			||||||
 | 
					          width: double.infinity,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      mediaContent = _controller.value.isInitialized
 | 
				
			||||||
 | 
					          ? CachedVideoPlayerPlus(_controller)
 | 
				
			||||||
 | 
					          : const Center(child: CircularProgressIndicator());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AspectRatio(
 | 
				
			||||||
 | 
					      aspectRatio: _controller.value.isInitialized
 | 
				
			||||||
 | 
					          ? _controller.value.aspectRatio
 | 
				
			||||||
 | 
					          : (isAudio ? 16 / 9 : 9 / 16),
 | 
				
			||||||
 | 
					      child: Stack(
 | 
				
			||||||
 | 
					        alignment: Alignment.center,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Center(
 | 
					          GestureDetector(onTap: _onTap, child: mediaContent),
 | 
				
			||||||
            child: AspectRatio(
 | 
					 | 
				
			||||||
              aspectRatio: _controller.value.isInitialized
 | 
					 | 
				
			||||||
                  ? _controller.value.aspectRatio
 | 
					 | 
				
			||||||
                  : 9 / 16,
 | 
					 | 
				
			||||||
              child: GestureDetector(
 | 
					 | 
				
			||||||
                onTap: _onTap,
 | 
					 | 
				
			||||||
                child: isAudio
 | 
					 | 
				
			||||||
                    ? CachedNetworkImage(
 | 
					 | 
				
			||||||
                        imageUrl: widget.details.coverUrl,
 | 
					 | 
				
			||||||
                        fit: BoxFit.cover,
 | 
					 | 
				
			||||||
                        placeholder: (context, url) =>
 | 
					 | 
				
			||||||
                            const CircularProgressIndicator(),
 | 
					 | 
				
			||||||
                        errorWidget: (context, url, error) => Image.asset(
 | 
					 | 
				
			||||||
                          'assets/images/music.webp',
 | 
					 | 
				
			||||||
                          fit: BoxFit.contain,
 | 
					 | 
				
			||||||
                          width: double.infinity,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    : _controller.value.isInitialized
 | 
					 | 
				
			||||||
                    ? CachedVideoPlayerPlus(_controller)
 | 
					 | 
				
			||||||
                    : const Center(child: CircularProgressIndicator()),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          if (_controller.value.isInitialized && _showControls)
 | 
					          if (_controller.value.isInitialized && _showControls)
 | 
				
			||||||
            Positioned.fill(
 | 
					            Positioned.fill(
 | 
				
			||||||
              child: GestureDetector(
 | 
					              child: GestureDetector(
 | 
				
			||||||
@@ -132,61 +127,18 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
                  color: Colors.black.withValues(alpha: 0.5),
 | 
					                  color: Colors.black.withValues(alpha: 0.5),
 | 
				
			||||||
                  child: VideoControlsOverlay(
 | 
					                  child: VideoControlsOverlay(
 | 
				
			||||||
                    controller: _controller,
 | 
					                    controller: _controller,
 | 
				
			||||||
                    button: () => _onTap(ctrlButton: true),
 | 
					                    onOverlayTap: () => _onTap(ctrlButton: true),
 | 
				
			||||||
 | 
					                    muted: muted,
 | 
				
			||||||
 | 
					                    onMuteToggle: () {
 | 
				
			||||||
 | 
					                      controller.toggleMuted();
 | 
				
			||||||
 | 
					                      setState(() {});
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      );
 | 
					      ),
 | 
				
			||||||
    } else {
 | 
					    );
 | 
				
			||||||
      return Column(
 | 
					 | 
				
			||||||
        mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
        children: [
 | 
					 | 
				
			||||||
          AspectRatio(
 | 
					 | 
				
			||||||
            aspectRatio: _controller.value.isInitialized
 | 
					 | 
				
			||||||
                ? _controller.value.aspectRatio
 | 
					 | 
				
			||||||
                : 9 / 16,
 | 
					 | 
				
			||||||
            child: Stack(
 | 
					 | 
				
			||||||
              alignment: Alignment.topCenter,
 | 
					 | 
				
			||||||
              children: [
 | 
					 | 
				
			||||||
                GestureDetector(
 | 
					 | 
				
			||||||
                  onTap: _onTap,
 | 
					 | 
				
			||||||
                  child: isAudio
 | 
					 | 
				
			||||||
                      ? CachedNetworkImage(
 | 
					 | 
				
			||||||
                          imageUrl: widget.details.coverUrl,
 | 
					 | 
				
			||||||
                          fit: BoxFit.cover,
 | 
					 | 
				
			||||||
                          placeholder: (context, url) =>
 | 
					 | 
				
			||||||
                              const CircularProgressIndicator(),
 | 
					 | 
				
			||||||
                          errorWidget: (context, url, error) => Image.asset(
 | 
					 | 
				
			||||||
                            'assets/images/music.webp',
 | 
					 | 
				
			||||||
                            fit: BoxFit.contain,
 | 
					 | 
				
			||||||
                            width: double.infinity,
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                      : _controller.value.isInitialized
 | 
					 | 
				
			||||||
                      ? CachedVideoPlayerPlus(_controller)
 | 
					 | 
				
			||||||
                      : const Center(child: CircularProgressIndicator()),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                if (_controller.value.isInitialized && _showControls) ...[
 | 
					 | 
				
			||||||
                  IgnorePointer(
 | 
					 | 
				
			||||||
                    ignoring: true,
 | 
					 | 
				
			||||||
                    child: Container(
 | 
					 | 
				
			||||||
                      color: Colors.black.withValues(alpha: 0.5),
 | 
					 | 
				
			||||||
                      width: double.infinity,
 | 
					 | 
				
			||||||
                      height: double.infinity,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  VideoControlsOverlay(
 | 
					 | 
				
			||||||
                    controller: _controller,
 | 
					 | 
				
			||||||
                    button: () => _onTap(ctrlButton: true),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ],
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -199,7 +199,7 @@ packages:
 | 
				
			|||||||
    source: sdk
 | 
					    source: sdk
 | 
				
			||||||
    version: "0.0.0"
 | 
					    version: "0.0.0"
 | 
				
			||||||
  flutter_cache_manager:
 | 
					  flutter_cache_manager:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_cache_manager
 | 
					      name: flutter_cache_manager
 | 
				
			||||||
      sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
 | 
					      sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
 | 
				
			||||||
@@ -448,6 +448,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.5.1"
 | 
					    version: "1.5.1"
 | 
				
			||||||
 | 
					  pullex:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: pullex
 | 
				
			||||||
 | 
					      sha256: f29a0b5eef4c16e32ae4b32cf6ad1a6eea0778d5bad8ee6cb29edb7d44496c1c
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.0.0"
 | 
				
			||||||
  rxdart:
 | 
					  rxdart:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    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
 | 
					# 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.3.4+60
 | 
					version: 1.4.0+61
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.9.0-100.2.beta
 | 
					  sdk: ^3.9.0-100.2.beta
 | 
				
			||||||
@@ -31,12 +31,13 @@ dependencies:
 | 
				
			|||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    sdk: flutter
 | 
					    sdk: flutter
 | 
				
			||||||
  http: ^1.4.0
 | 
					  http: ^1.4.0
 | 
				
			||||||
 | 
					  get: ^4.7.2
 | 
				
			||||||
 | 
					  encrypt_shared_preferences: ^0.9.9
 | 
				
			||||||
  cached_network_image: ^3.4.1
 | 
					  cached_network_image: ^3.4.1
 | 
				
			||||||
  cached_video_player_plus: ^3.0.3
 | 
					  cached_video_player_plus: ^3.0.3
 | 
				
			||||||
  share_plus: ^11.0.0
 | 
					  share_plus: ^11.0.0
 | 
				
			||||||
  encrypt_shared_preferences: ^0.9.9
 | 
					  flutter_cache_manager: ^3.4.1
 | 
				
			||||||
  get: ^4.7.2
 | 
					  pullex: ^1.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user