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/services.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/service/media_service.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/localization_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.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/controller/theme_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/detail_view.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/media_grid.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/mediagrid.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/login.dart';
 | 
			
		||||
 | 
			
		||||
void main() async {
 | 
			
		||||
  WidgetsFlutterBinding.ensureInitialized();
 | 
			
		||||
  await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
 | 
			
		||||
  await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
 | 
			
		||||
  await MyTranslations.loadTranslations();
 | 
			
		||||
  await AppVersion.init();
 | 
			
		||||
  Get.put(MediaService());
 | 
			
		||||
  Get.put(MediaController());
 | 
			
		||||
  Get.put(AuthController());
 | 
			
		||||
  LocalizationController localizationController = Get.put(LocalizationController());
 | 
			
		||||
  final MediaController mediaController = Get.put(MediaController());
 | 
			
		||||
  final ThemeController themeController = Get.put(ThemeController());
 | 
			
		||||
  final LocalizationController localizationController = Get.put(
 | 
			
		||||
    LocalizationController(),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Get.addTranslations(MyTranslations.instance.keys);
 | 
			
		||||
  Get.locale = localizationController.currentLocale.value;
 | 
			
		||||
@@ -37,16 +36,36 @@ void main() async {
 | 
			
		||||
        navigatorKey: Get.key,
 | 
			
		||||
        theme: themeController.currentTheme.value,
 | 
			
		||||
        debugShowCheckedModeBanner: false,
 | 
			
		||||
        initialRoute: '/',
 | 
			
		||||
        onGenerateRoute: (RouteSettings settings) {
 | 
			
		||||
          final uri = Uri.parse(settings.name ?? '/');
 | 
			
		||||
          final Uri uri = Uri.parse(settings.name ?? '/');
 | 
			
		||||
 | 
			
		||||
          if (uri.path == '/' || uri.pathSegments.isEmpty) {
 | 
			
		||||
            return MaterialPageRoute(builder: (_) => MediaGrid());
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (uri.path == '/login') {
 | 
			
		||||
            return MaterialPageRoute(builder: (_) => LoginScreen());
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (uri.pathSegments.length == 1) {
 | 
			
		||||
            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());
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 String dest;
 | 
			
		||||
  final int mode;
 | 
			
		||||
  final List<Tag> tags;
 | 
			
		||||
  final List<Tag>? tags;
 | 
			
		||||
  final List<Favorite>? favorites;
 | 
			
		||||
 | 
			
		||||
  MediaItem({
 | 
			
		||||
@@ -15,10 +15,15 @@ class MediaItem {
 | 
			
		||||
    required this.stamp,
 | 
			
		||||
    required this.dest,
 | 
			
		||||
    required this.mode,
 | 
			
		||||
    required this.tags,
 | 
			
		||||
    required this.favorites,
 | 
			
		||||
    this.tags = const [],
 | 
			
		||||
    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({
 | 
			
		||||
    int? id,
 | 
			
		||||
    String? mime,
 | 
			
		||||
@@ -42,27 +47,6 @@ class MediaItem {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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(
 | 
			
		||||
      id: json['id'],
 | 
			
		||||
      mime: json['mime'],
 | 
			
		||||
@@ -70,15 +54,18 @@ class MediaItem {
 | 
			
		||||
      stamp: json['stamp'],
 | 
			
		||||
      dest: json['dest'],
 | 
			
		||||
      mode: json['mode'],
 | 
			
		||||
      tags: parsedTags,
 | 
			
		||||
      favorites: parsedFavorites,
 | 
			
		||||
      tags:
 | 
			
		||||
          (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 {
 | 
			
		||||
@@ -99,15 +86,19 @@ class Tag {
 | 
			
		||||
 | 
			
		||||
class Favorite {
 | 
			
		||||
  final int userId;
 | 
			
		||||
  final String user;
 | 
			
		||||
  final String username;
 | 
			
		||||
  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) {
 | 
			
		||||
    return Favorite(
 | 
			
		||||
      userId: json['user_id'],
 | 
			
		||||
      user: json['user'],
 | 
			
		||||
      username: json['user'],
 | 
			
		||||
      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:f0ckapp/models/media_item.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_widget.dart';
 | 
			
		||||
 | 
			
		||||
class FullScreenMediaView extends StatefulWidget {
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
 | 
			
		||||
class LoginPage extends StatefulWidget {
 | 
			
		||||
  const LoginPage({super.key});
 | 
			
		||||
class LoginScreen extends StatefulWidget {
 | 
			
		||||
  const LoginScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<StatefulWidget> createState() => _LoginPageState();
 | 
			
		||||
  State<LoginScreen> createState() => _LoginScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _LoginPageState extends State<LoginPage> {
 | 
			
		||||
  final AuthController authController = Get.find();
 | 
			
		||||
class _LoginScreenState extends State<LoginScreen> {
 | 
			
		||||
  final AuthController authController = Get.put(AuthController());
 | 
			
		||||
  final TextEditingController usernameController = TextEditingController();
 | 
			
		||||
  final TextEditingController passwordController = TextEditingController();
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +21,7 @@ class _LoginPageState extends State<LoginPage> {
 | 
			
		||||
  void _showMsg(String message, {String title = ''}) {
 | 
			
		||||
    Get
 | 
			
		||||
      ..closeAllSnackbars()
 | 
			
		||||
      ..snackbar(message, title, snackPosition: SnackPosition.BOTTOM);
 | 
			
		||||
      ..snackbar(title, message, snackPosition: SnackPosition.BOTTOM);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -80,6 +81,11 @@ class _LoginPageState extends State<LoginPage> {
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const SizedBox(height: 24),
 | 
			
		||||
                  if (authController.error.value != null)
 | 
			
		||||
                    Text(
 | 
			
		||||
                      authController.error.value!,
 | 
			
		||||
                      style: const TextStyle(color: Colors.red),
 | 
			
		||||
                    ),
 | 
			
		||||
                  SizedBox(
 | 
			
		||||
                    width: double.infinity,
 | 
			
		||||
                    child: ElevatedButton(
 | 
			
		||||
@@ -87,16 +93,23 @@ class _LoginPageState extends State<LoginPage> {
 | 
			
		||||
                          ? null
 | 
			
		||||
                          : () async {
 | 
			
		||||
                              setState(() => _isLoading = true);
 | 
			
		||||
                              final success = await authController.login(
 | 
			
		||||
                              final bool success = await authController.login(
 | 
			
		||||
                                usernameController.text,
 | 
			
		||||
                                passwordController.text,
 | 
			
		||||
                              );
 | 
			
		||||
                              setState(() => _isLoading = false);
 | 
			
		||||
                              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
 | 
			
		||||
                          ? 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:get/get.dart';
 | 
			
		||||
import 'package:pullex/pullex.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/filter_bar.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/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class MediaGrid extends StatefulWidget {
 | 
			
		||||
  const MediaGrid({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<MediaGrid> createState() => _MediaGridState();
 | 
			
		||||
  State<MediaGrid> createState() => _MediaGrid();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
  final MediaController controller = Get.find<MediaController>();
 | 
			
		||||
class _MediaGrid extends State<MediaGrid> {
 | 
			
		||||
  final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  Timer? _debounce;
 | 
			
		||||
  final MediaController _mediaController = Get.put(MediaController());
 | 
			
		||||
  final PullexRefreshController _refreshController = PullexRefreshController(
 | 
			
		||||
    initialRefresh: false,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    controller.loadMediaItems();
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    _mediaController.fetchInitial();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      endDrawer: EndDrawer(),
 | 
			
		||||
      endDrawerEnableOpenDragGesture: controller.drawerSwipeEnabled.value,
 | 
			
		||||
      body: RefreshIndicator(
 | 
			
		||||
        edgeOffset: 100,
 | 
			
		||||
      endDrawerEnableOpenDragGesture: _mediaController.drawerSwipeEnabled.value,
 | 
			
		||||
      bottomNavigationBar: FilterBar(scrollController: _scrollController),
 | 
			
		||||
      body: PullexRefresh(
 | 
			
		||||
        controller: _refreshController,
 | 
			
		||||
        enablePullDown: true,
 | 
			
		||||
        enablePullUp: true,
 | 
			
		||||
        header: MaterialHeader(offset: 140),
 | 
			
		||||
        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(
 | 
			
		||||
          controller: _scrollController,
 | 
			
		||||
          slivers: [
 | 
			
		||||
            SliverAppBar(
 | 
			
		||||
              floating: true,
 | 
			
		||||
              pinned: false,
 | 
			
		||||
              snap: true,
 | 
			
		||||
              floating: true,
 | 
			
		||||
              title: GestureDetector(
 | 
			
		||||
                child: Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
@@ -75,7 +82,7 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  controller.setTag(null);
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
              actions: [
 | 
			
		||||
@@ -91,16 +98,15 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
                Obx(
 | 
			
		||||
                  () => IconButton(
 | 
			
		||||
                    icon: Icon(
 | 
			
		||||
                      controller.random.value
 | 
			
		||||
                      _mediaController.random.value == 1
 | 
			
		||||
                          ? Icons.shuffle_on_outlined
 | 
			
		||||
                          : Icons.shuffle,
 | 
			
		||||
                    ),
 | 
			
		||||
                    onPressed: () async {
 | 
			
		||||
                      await controller.toggleRandom();
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      _mediaController.toggleRandom();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
                Builder(
 | 
			
		||||
                  builder: (context) {
 | 
			
		||||
                    return IconButton(
 | 
			
		||||
@@ -113,14 +119,21 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            SliverPadding(
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              sliver: Obx(
 | 
			
		||||
                () => SliverGrid(
 | 
			
		||||
            Obx(
 | 
			
		||||
              () => SliverPadding(
 | 
			
		||||
                padding: const EdgeInsets.all(4),
 | 
			
		||||
                sliver: SliverGrid(
 | 
			
		||||
                  delegate: SliverChildBuilderDelegate((context, index) {
 | 
			
		||||
                    return MediaTile(item: controller.mediaItems[index]);
 | 
			
		||||
                  }, childCount: controller.mediaItems.length),
 | 
			
		||||
                  gridDelegate: controller.crossAxisCount.value == 0
 | 
			
		||||
                    final item = _mediaController.filteredItems[index];
 | 
			
		||||
                    return GestureDetector(
 | 
			
		||||
                      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(
 | 
			
		||||
                          maxCrossAxisExtent: 150,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
@@ -128,7 +141,7 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
                        )
 | 
			
		||||
                      : SliverGridDelegateWithFixedCrossAxisCount(
 | 
			
		||||
                          crossAxisCount: controller.crossAxisCount.value,
 | 
			
		||||
                          crossAxisCount: _mediaController.crossAxisCount.value,
 | 
			
		||||
                          crossAxisSpacing: 5,
 | 
			
		||||
                          mainAxisSpacing: 5,
 | 
			
		||||
                          childAspectRatio: 1,
 | 
			
		||||
@@ -141,12 +154,12 @@ class _MediaGridState extends State<MediaGrid> {
 | 
			
		||||
      ),
 | 
			
		||||
      persistentFooterButtons: [
 | 
			
		||||
        Obx(() {
 | 
			
		||||
          if (controller.tag.value != null) {
 | 
			
		||||
          if (_mediaController.tag.value != null) {
 | 
			
		||||
            return Center(
 | 
			
		||||
              child: InputChip(
 | 
			
		||||
                label: Text(controller.tag.value!),
 | 
			
		||||
                onDeleted: () async {
 | 
			
		||||
                  await controller.setTag(null);
 | 
			
		||||
                label: Text(_mediaController.tag.value!),
 | 
			
		||||
                onDeleted: () {
 | 
			
		||||
                  _mediaController.setTag(null);
 | 
			
		||||
                  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:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/localization_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/localizationcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/animatedtransition.dart';
 | 
			
		||||
 | 
			
		||||
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,6 +1,6 @@
 | 
			
		||||
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 }
 | 
			
		||||
 | 
			
		||||
@@ -10,7 +10,7 @@ Widget buildAnimatedTransition({
 | 
			
		||||
  required PageController pageController,
 | 
			
		||||
  required int index,
 | 
			
		||||
  required MediaController controller,
 | 
			
		||||
  }) {
 | 
			
		||||
}) {
 | 
			
		||||
  final double value = pageController.position.haveDimensions
 | 
			
		||||
      ? pageController.page! - index
 | 
			
		||||
      : 0;
 | 
			
		||||
@@ -47,4 +47,4 @@ Widget buildAnimatedTransition({
 | 
			
		||||
        child: child,
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,13 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
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> {
 | 
			
		||||
  final MediaController controller = Get.find<MediaController>();
 | 
			
		||||
@@ -144,8 +144,8 @@ class CustomSearchDelegate extends SearchDelegate<String> {
 | 
			
		||||
                'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
 | 
			
		||||
                style: TextStyle(fontSize: 12),
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () async {
 | 
			
		||||
                await controller.setTag(suggestion.tag);
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                controller.setTag(suggestion.tag);
 | 
			
		||||
                close(context, suggestion.tag);
 | 
			
		||||
              },
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/media_item.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
 | 
			
		||||
class ActionTag extends StatelessWidget {
 | 
			
		||||
  final Tag tag;
 | 
			
		||||
@@ -13,13 +13,7 @@ class ActionTag extends StatelessWidget {
 | 
			
		||||
    return GestureDetector(
 | 
			
		||||
      onTap: () => onTagTap(tag.tag),
 | 
			
		||||
      child:
 | 
			
		||||
          [
 | 
			
		||||
            'german',
 | 
			
		||||
            'dutch',
 | 
			
		||||
            'ukraine',
 | 
			
		||||
            'russia',
 | 
			
		||||
            'belgium',
 | 
			
		||||
          ].contains(tag.tag)
 | 
			
		||||
          ['german', 'dutch', 'ukraine', 'russia', 'belgium'].contains(tag.tag)
 | 
			
		||||
          ? Stack(
 | 
			
		||||
              alignment: Alignment.center,
 | 
			
		||||
              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:f0ckapp/screens/login_screen.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/auth_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/settings_screen.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/theme_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/authcontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/themecontroller.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/login.dart';
 | 
			
		||||
import 'package:f0ckapp/screens/settings.dart';
 | 
			
		||||
import 'package:f0ckapp/utils/appversion.dart';
 | 
			
		||||
 | 
			
		||||
class EndDrawer extends StatelessWidget {
 | 
			
		||||
@@ -89,7 +89,7 @@ class EndDrawer extends StatelessWidget {
 | 
			
		||||
                    ElevatedButton(
 | 
			
		||||
                      onPressed: () {
 | 
			
		||||
                        Navigator.pop(context);
 | 
			
		||||
                        Get.to(() => LoginPage());
 | 
			
		||||
                        Get.to(() => LoginScreen());
 | 
			
		||||
                      },
 | 
			
		||||
                      child: const Text('Login'),
 | 
			
		||||
                    ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
 | 
			
		||||
class FavoriteAvatars extends StatelessWidget {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,12 @@ import 'package:flutter/material.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/controller/media_controller.dart';
 | 
			
		||||
import 'package:f0ckapp/service/media_service.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class FilterBar extends StatelessWidget {
 | 
			
		||||
  final ScrollController scrollController;
 | 
			
		||||
 | 
			
		||||
  const FilterBar({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.scrollController,
 | 
			
		||||
  });
 | 
			
		||||
  const FilterBar({super.key, required this.scrollController});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
@@ -23,8 +19,9 @@ class FilterBar extends StatelessWidget {
 | 
			
		||||
        mainAxisAlignment: MainAxisAlignment.spaceAround,
 | 
			
		||||
        children: [
 | 
			
		||||
          const Text('type: '),
 | 
			
		||||
          Obx(() => DropdownButton<String>(
 | 
			
		||||
            value: mediaTypes[c.type.value],
 | 
			
		||||
          Obx(
 | 
			
		||||
            () => DropdownButton<String>(
 | 
			
		||||
              value: mediaTypes[c.typeIndex.value],
 | 
			
		||||
              isDense: true,
 | 
			
		||||
              items: mediaTypes.map((String value) {
 | 
			
		||||
                return DropdownMenuItem<String>(
 | 
			
		||||
@@ -34,14 +31,16 @@ class FilterBar extends StatelessWidget {
 | 
			
		||||
              }).toList(),
 | 
			
		||||
              onChanged: (String? newValue) {
 | 
			
		||||
                if (newValue != null) {
 | 
			
		||||
                c.setType(mediaTypes.indexOf(newValue));
 | 
			
		||||
                  c.setTypeIndex(mediaTypes.indexOf(newValue));
 | 
			
		||||
                  scrollController.jumpTo(0);
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
          )),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          const Text('mode: '),
 | 
			
		||||
          Obx(() => DropdownButton<String>(
 | 
			
		||||
            value: mediaModes[c.mode.value],
 | 
			
		||||
          Obx(
 | 
			
		||||
            () => DropdownButton<String>(
 | 
			
		||||
              value: mediaModes[c.modeIndex.value],
 | 
			
		||||
              isDense: true,
 | 
			
		||||
              items: mediaModes.map((String value) {
 | 
			
		||||
                return DropdownMenuItem<String>(
 | 
			
		||||
@@ -51,11 +50,12 @@ class FilterBar extends StatelessWidget {
 | 
			
		||||
              }).toList(),
 | 
			
		||||
              onChanged: (String? newValue) {
 | 
			
		||||
                if (newValue != null) {
 | 
			
		||||
                c.setMode(mediaModes.indexOf(newValue));
 | 
			
		||||
                  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:get/get.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:f0ckapp/models/media_item.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
 | 
			
		||||
class MediaTile extends StatelessWidget {
 | 
			
		||||
  final MediaItem item;
 | 
			
		||||
 | 
			
		||||
  const MediaTile({super.key, required this.item});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -20,14 +19,12 @@ class MediaTile extends StatelessWidget {
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          fit: StackFit.expand,
 | 
			
		||||
          children: [
 | 
			
		||||
            Hero(
 | 
			
		||||
              tag: 'media-${item.id}',
 | 
			
		||||
              child: CachedNetworkImage(
 | 
			
		||||
                imageUrl: item.thumbnailUrl,
 | 
			
		||||
            CachedNetworkImage(
 | 
			
		||||
              imageUrl: 'https://f0ck.me/t/${item.id}.webp',
 | 
			
		||||
              fit: BoxFit.cover,
 | 
			
		||||
                placeholder: (content, url) => Container(color: Colors.grey[900]),
 | 
			
		||||
                errorWidget: (context, url, error) => const Icon(Icons.error),
 | 
			
		||||
              ),
 | 
			
		||||
              placeholder: (context, url) => Container(color: Colors.grey[900]),
 | 
			
		||||
              errorWidget: (context, url, error) =>
 | 
			
		||||
                  const Icon(Icons.broken_image),
 | 
			
		||||
            ),
 | 
			
		||||
            Align(
 | 
			
		||||
              alignment: Alignment.bottomRight,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,66 +1,50 @@
 | 
			
		||||
import 'package:flutter/material.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 {
 | 
			
		||||
  final CachedVideoPlayerPlusController controller;
 | 
			
		||||
  final VoidCallback button;
 | 
			
		||||
  final VoidCallback onOverlayTap;
 | 
			
		||||
  final bool muted;
 | 
			
		||||
  final VoidCallback onMuteToggle;
 | 
			
		||||
 | 
			
		||||
  const VideoControlsOverlay({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.controller,
 | 
			
		||||
    required this.button,
 | 
			
		||||
    required this.onOverlayTap,
 | 
			
		||||
    required this.muted,
 | 
			
		||||
    required this.onMuteToggle,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final MediaController c = Get.find<MediaController>();
 | 
			
		||||
 | 
			
		||||
    return Stack(
 | 
			
		||||
      alignment: Alignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
        Positioned(
 | 
			
		||||
          right: 12,
 | 
			
		||||
          bottom: 12,
 | 
			
		||||
          child: Obx(
 | 
			
		||||
            () => _ControlButton(
 | 
			
		||||
              c.muted.value ? Icons.volume_off : Icons.volume_up,
 | 
			
		||||
              () async {
 | 
			
		||||
                button();
 | 
			
		||||
                await c.toggleMuted();
 | 
			
		||||
              },
 | 
			
		||||
              size: 16,
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        Center(
 | 
			
		||||
          child: Row(
 | 
			
		||||
        Row(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            _ControlButton(Icons.replay_10, () {
 | 
			
		||||
                button();
 | 
			
		||||
              onOverlayTap();
 | 
			
		||||
              Duration newPosition =
 | 
			
		||||
                  controller.value.position - const Duration(seconds: 10);
 | 
			
		||||
              if (newPosition < Duration.zero) newPosition = Duration.zero;
 | 
			
		||||
              controller.seekTo(newPosition);
 | 
			
		||||
            }),
 | 
			
		||||
              SizedBox(width: 40),
 | 
			
		||||
            const SizedBox(width: 40),
 | 
			
		||||
            _ControlButton(
 | 
			
		||||
              controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
			
		||||
              () {
 | 
			
		||||
                  button();
 | 
			
		||||
                onOverlayTap();
 | 
			
		||||
                controller.value.isPlaying
 | 
			
		||||
                    ? controller.pause()
 | 
			
		||||
                    : controller.play();
 | 
			
		||||
              },
 | 
			
		||||
              size: 64,
 | 
			
		||||
            ),
 | 
			
		||||
              SizedBox(width: 40),
 | 
			
		||||
            const SizedBox(width: 40),
 | 
			
		||||
            _ControlButton(Icons.forward_10, () {
 | 
			
		||||
                button();
 | 
			
		||||
              onOverlayTap();
 | 
			
		||||
              Duration newPosition =
 | 
			
		||||
                  controller.value.position + const Duration(seconds: 10);
 | 
			
		||||
              if (newPosition > controller.value.duration) {
 | 
			
		||||
@@ -70,6 +54,13 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
        Positioned(
 | 
			
		||||
          right: 12,
 | 
			
		||||
          bottom: 12,
 | 
			
		||||
          child: _ControlButton(muted ? Icons.volume_off : Icons.volume_up, () {
 | 
			
		||||
            onOverlayTap();
 | 
			
		||||
            onMuteToggle();
 | 
			
		||||
          }, size: 16),
 | 
			
		||||
        ),
 | 
			
		||||
        Align(
 | 
			
		||||
          alignment: Alignment.bottomCenter,
 | 
			
		||||
@@ -83,11 +74,12 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
			
		||||
                  bottom: 12,
 | 
			
		||||
                  child: Text(
 | 
			
		||||
                    '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
 | 
			
		||||
                    style: const TextStyle(color: Colors.white, fontSize: 12),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                Listener(
 | 
			
		||||
                  onPointerDown: (_) {
 | 
			
		||||
                    button();
 | 
			
		||||
                    onOverlayTap();
 | 
			
		||||
                  },
 | 
			
		||||
                  child: VideoProgressIndicator(
 | 
			
		||||
                    controller,
 | 
			
		||||
@@ -100,6 +92,7 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                if (controller.value.duration.inMilliseconds > 0)
 | 
			
		||||
                  Positioned(
 | 
			
		||||
                    left:
 | 
			
		||||
                        (controller.value.position.inMilliseconds /
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
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: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/media_item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/videooverlay_widget.dart';
 | 
			
		||||
import 'package:f0ckapp/models/item.dart';
 | 
			
		||||
import 'package:f0ckapp/widgets/video_controls_overlay.dart';
 | 
			
		||||
import 'package:f0ckapp/controller/mediacontroller.dart';
 | 
			
		||||
 | 
			
		||||
class VideoWidget extends StatefulWidget {
 | 
			
		||||
  final MediaItem details;
 | 
			
		||||
@@ -44,15 +44,13 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
    );
 | 
			
		||||
    await _controller.initialize();
 | 
			
		||||
    setState(() {});
 | 
			
		||||
 | 
			
		||||
    _controller.addListener(() => setState(() {}));
 | 
			
		||||
    _controller.setLooping(true);
 | 
			
		||||
    _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
 | 
			
		||||
 | 
			
		||||
    if (widget.isActive) {
 | 
			
		||||
      _controller.play();
 | 
			
		||||
    }
 | 
			
		||||
    _controller.setLooping(true);
 | 
			
		||||
 | 
			
		||||
    _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -96,34 +94,31 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
 | 
			
		||||
    bool isAudio = widget.details.mime.startsWith('audio');
 | 
			
		||||
 | 
			
		||||
    if (widget.fullScreen) {
 | 
			
		||||
      return Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          Center(
 | 
			
		||||
            child: AspectRatio(
 | 
			
		||||
              aspectRatio: _controller.value.isInitialized
 | 
			
		||||
                  ? _controller.value.aspectRatio
 | 
			
		||||
                  : 9 / 16,
 | 
			
		||||
              child: GestureDetector(
 | 
			
		||||
                onTap: _onTap,
 | 
			
		||||
                child: isAudio
 | 
			
		||||
                    ? CachedNetworkImage(
 | 
			
		||||
    Widget mediaContent;
 | 
			
		||||
    if (isAudio) {
 | 
			
		||||
      mediaContent = CachedNetworkImage(
 | 
			
		||||
        imageUrl: widget.details.coverUrl,
 | 
			
		||||
        fit: BoxFit.cover,
 | 
			
		||||
                        placeholder: (context, url) =>
 | 
			
		||||
                            const CircularProgressIndicator(),
 | 
			
		||||
                        errorWidget: (context, url, error) => Image.asset(
 | 
			
		||||
        errorWidget: (c, e, s) => Image.asset(
 | 
			
		||||
          'assets/images/music.webp',
 | 
			
		||||
          fit: BoxFit.contain,
 | 
			
		||||
          width: double.infinity,
 | 
			
		||||
        ),
 | 
			
		||||
                      )
 | 
			
		||||
                    : _controller.value.isInitialized
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      mediaContent = _controller.value.isInitialized
 | 
			
		||||
          ? CachedVideoPlayerPlus(_controller)
 | 
			
		||||
                    : const Center(child: CircularProgressIndicator()),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          : const Center(child: CircularProgressIndicator());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AspectRatio(
 | 
			
		||||
      aspectRatio: _controller.value.isInitialized
 | 
			
		||||
          ? _controller.value.aspectRatio
 | 
			
		||||
          : (isAudio ? 16 / 9 : 9 / 16),
 | 
			
		||||
      child: Stack(
 | 
			
		||||
        alignment: Alignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          GestureDetector(onTap: _onTap, child: mediaContent),
 | 
			
		||||
          if (_controller.value.isInitialized && _showControls)
 | 
			
		||||
            Positioned.fill(
 | 
			
		||||
              child: GestureDetector(
 | 
			
		||||
@@ -132,61 +127,18 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
			
		||||
                  color: Colors.black.withValues(alpha: 0.5),
 | 
			
		||||
                  child: VideoControlsOverlay(
 | 
			
		||||
                    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
 | 
			
		||||
    version: "0.0.0"
 | 
			
		||||
  flutter_cache_manager:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_cache_manager
 | 
			
		||||
      sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
 | 
			
		||||
@@ -448,6 +448,14 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.5.1"
 | 
			
		||||
  pullex:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: pullex
 | 
			
		||||
      sha256: f29a0b5eef4c16e32ae4b32cf6ad1a6eea0778d5bad8ee6cb29edb7d44496c1c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.0"
 | 
			
		||||
  rxdart:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 1.3.4+60
 | 
			
		||||
version: 1.4.0+61
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.9.0-100.2.beta
 | 
			
		||||
@@ -31,12 +31,13 @@ dependencies:
 | 
			
		||||
  flutter:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
  http: ^1.4.0
 | 
			
		||||
 | 
			
		||||
  get: ^4.7.2
 | 
			
		||||
  encrypt_shared_preferences: ^0.9.9
 | 
			
		||||
  cached_network_image: ^3.4.1
 | 
			
		||||
  cached_video_player_plus: ^3.0.3
 | 
			
		||||
  share_plus: ^11.0.0
 | 
			
		||||
  encrypt_shared_preferences: ^0.9.9
 | 
			
		||||
  get: ^4.7.2
 | 
			
		||||
  flutter_cache_manager: ^3.4.1
 | 
			
		||||
  pullex: ^1.0.0
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user