From 2b5aaad33186c2dafd1c985294127ef20fd24ec3 Mon Sep 17 00:00:00 2001 From: Flummi Date: Thu, 19 Jun 2025 21:45:00 +0200 Subject: [PATCH] v1.4.0+61 --- lib/controller/auth_controller.dart | 98 ------ lib/controller/authcontroller.dart | 114 +++++++ ...oller.dart => localizationcontroller.dart} | 0 lib/controller/media_controller.dart | 112 ------- lib/controller/mediacontroller.dart | 181 ++++++++++ ...e_controller.dart => themecontroller.dart} | 0 lib/main.dart | 49 ++- lib/models/feed.dart | 17 + lib/models/{media_item.dart => item.dart} | 59 ++-- ...{suggestion_model.dart => suggestion.dart} | 0 lib/screens/detail_view.dart | 293 ---------------- ...fullscreen_screen.dart => fullscreen.dart} | 2 +- lib/screens/{login_screen.dart => login.dart} | 35 +- lib/screens/mediadetail.dart | 312 ++++++++++++++++++ .../{media_grid.dart => mediagrid.dart} | 114 ++++--- .../{settings_screen.dart => settings.dart} | 4 +- lib/service/media_service.dart | 107 ------ lib/services/api.dart | 86 +++++ lib/utils/animatedtransition.dart | 84 ++--- lib/utils/customsearchdelegate.dart | 8 +- lib/widgets/actiontag.dart | 10 +- lib/widgets/detailmediacontent.dart | 121 ------- lib/widgets/end_drawer.dart | 10 +- lib/widgets/favorite_avatars.dart | 1 + lib/widgets/filter_bar.dart | 78 ++--- lib/widgets/media_tile.dart | 17 +- ...idget.dart => video_controls_overlay.dart} | 129 ++++---- lib/widgets/video_widget.dart | 126 +++---- pubspec.lock | 10 +- pubspec.yaml | 9 +- 30 files changed, 1073 insertions(+), 1113 deletions(-) delete mode 100644 lib/controller/auth_controller.dart create mode 100644 lib/controller/authcontroller.dart rename lib/controller/{localization_controller.dart => localizationcontroller.dart} (100%) delete mode 100644 lib/controller/media_controller.dart create mode 100644 lib/controller/mediacontroller.dart rename lib/controller/{theme_controller.dart => themecontroller.dart} (100%) create mode 100644 lib/models/feed.dart rename lib/models/{media_item.dart => item.dart} (68%) rename lib/models/{suggestion_model.dart => suggestion.dart} (100%) delete mode 100644 lib/screens/detail_view.dart rename lib/screens/{fullscreen_screen.dart => fullscreen.dart} (97%) rename lib/screens/{login_screen.dart => login.dart} (75%) create mode 100644 lib/screens/mediadetail.dart rename lib/screens/{media_grid.dart => mediagrid.dart} (56%) rename lib/screens/{settings_screen.dart => settings.dart} (98%) delete mode 100644 lib/service/media_service.dart create mode 100644 lib/services/api.dart delete mode 100644 lib/widgets/detailmediacontent.dart rename lib/widgets/{videooverlay_widget.dart => video_controls_overlay.dart} (50%) diff --git a/lib/controller/auth_controller.dart b/lib/controller/auth_controller.dart deleted file mode 100644 index 62d3fdc..0000000 --- a/lib/controller/auth_controller.dart +++ /dev/null @@ -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(); - - RxnString token = RxnString(); - RxnInt userId = RxnInt(); - RxnString avatarUrl = RxnString(); - RxnString username = RxnString(); - - @override - void onInit() { - super.onInit(); - loadToken(); - } - - Future loadToken() async { - token.value = await storage.getString('token'); - if (token.value != null) { - await fetchUserInfo(); - } - } - - Future saveToken(String newToken) async { - token.value = newToken; - await storage.setString('token', newToken); - await fetchUserInfo(); - } - - Future 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 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 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(); - } - } -} diff --git a/lib/controller/authcontroller.dart b/lib/controller/authcontroller.dart new file mode 100644 index 0000000..41064a4 --- /dev/null +++ b/lib/controller/authcontroller.dart @@ -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 loadToken() async { + token.value = await storage.getString('token'); + if (token.value != null) { + await fetchUserInfo(); + } + } + + Future saveToken(String newToken) async { + token.value = newToken; + await storage.setString('token', newToken); + await fetchUserInfo(); + } + + Future 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 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 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; +} diff --git a/lib/controller/localization_controller.dart b/lib/controller/localizationcontroller.dart similarity index 100% rename from lib/controller/localization_controller.dart rename to lib/controller/localizationcontroller.dart diff --git a/lib/controller/media_controller.dart b/lib/controller/media_controller.dart deleted file mode 100644 index c462add..0000000 --- a/lib/controller/media_controller.dart +++ /dev/null @@ -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 mediaItems = [].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 transitionType = PageTransition.opacity.obs; - - MediaItem? selectedItem; - - @override - void onInit() async { - super.onInit(); - await loadSettings(); - } - - Future 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 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 setTag(String? newTag) async { - tag.value = newTag; - await loadMediaItems(); - } - - Future setType(int newType) async { - type.value = newType; - await loadMediaItems(); - } - - Future setMode(int newMode) async { - mode.value = newMode; - await loadMediaItems(); - } - - Future toggleRandom() async { - random.value = !random.value; - await loadMediaItems(); - } - - Future toggleMuted() async { - muted.value = !muted.value; - await saveSettings(); - } - - Future setCrossAxisCount(int newCrossAxisCount) async { - crossAxisCount.value = newCrossAxisCount; - await saveSettings(); - } - - Future setDrawerSwipeEnabled(bool newValue) async { - drawerSwipeEnabled.value = newValue; - await saveSettings(); - } - - Future setTransitionType(PageTransition newType) async { - transitionType.value = newType; - await saveSettings(); - } - - Future loadMediaItems({int? older, bool append = false}) async { - if (isLoading.value) return; - try { - isLoading.value = true; - final List 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; - } - } -} diff --git a/lib/controller/mediacontroller.dart b/lib/controller/mediacontroller.dart new file mode 100644 index 0000000..4fa49ca --- /dev/null +++ b/lib/controller/mediacontroller.dart @@ -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 mediaTypes = ["alles", "image", "video", "audio"]; +const List mediaModes = ["sfw", "nsfw", "untagged", "all"]; + +class MediaController extends GetxController { + final ApiService _api = ApiService(); + final EncryptedSharedPreferencesAsync storage = + EncryptedSharedPreferencesAsync.getInstance(); + + RxList items = [].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 tag = Rxn(); + RxBool muted = false.obs; + Rx 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 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?> toggleFavorite( + MediaItem item, + bool isFavorite, + ) async { + try { + return await _api.toggleFavorite(item, isFavorite); + } catch (e) { + return []; + } + } + + Future 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 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 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 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 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 setTransitionType(PageTransition type) async { + transitionType.value = type; + await saveSettings(); + } + + Future setCrossAxisCount(int value) async { + crossAxisCount.value = value; + await saveSettings(); + } + + Future 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 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 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); + } +} diff --git a/lib/controller/theme_controller.dart b/lib/controller/themecontroller.dart similarity index 100% rename from lib/controller/theme_controller.dart rename to lib/controller/themecontroller.dart diff --git a/lib/main.dart b/lib/main.dart index 597200e..3cdf76a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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()); diff --git a/lib/models/feed.dart b/lib/models/feed.dart new file mode 100644 index 0000000..8de2056 --- /dev/null +++ b/lib/models/feed.dart @@ -0,0 +1,17 @@ +import 'package:f0ckapp/models/item.dart'; + +class Feed { + final bool atEnd; + final bool atStart; + final List items; + + Feed({required this.atEnd, required this.atStart, required this.items}); + + factory Feed.fromJson(Map json) { + return Feed( + atEnd: json['atEnd'] ?? false, + atStart: json['atStart'] ?? false, + items: (json['items'] as List).map((e) => MediaItem.fromJson(e)).toList(), + ); + } +} diff --git a/lib/models/media_item.dart b/lib/models/item.dart similarity index 68% rename from lib/models/media_item.dart rename to lib/models/item.dart index 477f6f0..b2f8fe1 100644 --- a/lib/models/media_item.dart +++ b/lib/models/item.dart @@ -5,7 +5,7 @@ class MediaItem { final int stamp; final String dest; final int mode; - final List tags; + final List? tags; final List? 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 json) { - List parsedTags = []; - if (json['tags'] is List) { - parsedTags = (json['tags'] as List) - .map((tagJson) => Tag.fromJson(tagJson as Map)) - .toList(); - } else { - parsedTags = []; - } - - List parsedFavorites = []; - if (json['favorites'] is List) { - parsedFavorites = (json['favorites'] as List) - .map( - (favoritesJson) => - Favorite.fromJson(favoritesJson as Map), - ) - .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?) + ?.map((e) => Tag.fromJson(e)) + .toList() ?? + [], + favorites: + (json['favorites'] as List?) + ?.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 json) { return Favorite( userId: json['user_id'], - user: json['user'], + username: json['user'], avatar: json['avatar'], ); } diff --git a/lib/models/suggestion_model.dart b/lib/models/suggestion.dart similarity index 100% rename from lib/models/suggestion_model.dart rename to lib/models/suggestion.dart diff --git a/lib/screens/detail_view.dart b/lib/screens/detail_view.dart deleted file mode 100644 index b0c5e4b..0000000 --- a/lib/screens/detail_view.dart +++ /dev/null @@ -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 createState() => _DetailViewState(); -} - -class _DetailViewState extends State { - final MediaController controller = Get.find(); - 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 _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 _downloadMedia(MediaItem item) async { - final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl); - final MethodChannel methodChannel = const MethodChannel('MediaShit'); - - bool? success = await methodChannel.invokeMethod('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 _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(); - - 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( - 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('/'); - }, - ), - ), - ); - }, - ), - ), - ], - ), - ); - } -} diff --git a/lib/screens/fullscreen_screen.dart b/lib/screens/fullscreen.dart similarity index 97% rename from lib/screens/fullscreen_screen.dart rename to lib/screens/fullscreen.dart index 8a271ec..a2e5334 100644 --- a/lib/screens/fullscreen_screen.dart +++ b/lib/screens/fullscreen.dart @@ -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 { diff --git a/lib/screens/login_screen.dart b/lib/screens/login.dart similarity index 75% rename from lib/screens/login_screen.dart rename to lib/screens/login.dart index 7771974..deac4ed 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login.dart @@ -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 createState() => _LoginPageState(); + State createState() => _LoginScreenState(); } -class _LoginPageState extends State { - final AuthController authController = Get.find(); +class _LoginScreenState extends State { + final AuthController authController = Get.put(AuthController()); final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); @@ -20,7 +21,7 @@ class _LoginPageState extends State { 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 { ), ), 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 { ? 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( diff --git a/lib/screens/mediadetail.dart b/lib/screens/mediadetail.dart new file mode 100644 index 0000000..6177eea --- /dev/null +++ b/lib/screens/mediadetail.dart @@ -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 createState() => _MediaDetailScreenState(); +} + +class _MediaDetailScreenState extends State { + late PageController _pageController; + final MediaController mediaController = Get.find(); + final AuthController authController = Get.find(); + 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 _downloadMedia(MediaItem item) async { + final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl); + final MethodChannel methodChannel = const MethodChannel('MediaShit'); + + bool? success = await methodChannel.invokeMethod('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( + 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? 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(); + } + }), + ], + ); + }, + ), + ); + } +} diff --git a/lib/screens/media_grid.dart b/lib/screens/mediagrid.dart similarity index 56% rename from lib/screens/media_grid.dart rename to lib/screens/mediagrid.dart index f8c3c99..75d6b9b 100644 --- a/lib/screens/media_grid.dart +++ b/lib/screens/mediagrid.dart @@ -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 createState() => _MediaGridState(); + State createState() => _MediaGrid(); } -class _MediaGridState extends State { - final MediaController controller = Get.find(); +class _MediaGrid extends State { 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 { ], ), onTap: () { - controller.setTag(null); + _mediaController.setTag(null); }, ), actions: [ @@ -91,16 +98,15 @@ class _MediaGridState extends State { 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 { ), ], ), - 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 { 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 { ), 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 { } }), ], - bottomNavigationBar: FilterBar(scrollController: _scrollController), ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings.dart similarity index 98% rename from lib/screens/settings_screen.dart rename to lib/screens/settings.dart index 9a6a507..320ef9e 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings.dart @@ -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 { diff --git a/lib/service/media_service.dart b/lib/service/media_service.dart deleted file mode 100644 index 3860d3a..0000000 --- a/lib/service/media_service.dart +++ /dev/null @@ -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 mediaTypes = ["alles", "image", "video", "audio"]; -const List mediaModes = ["sfw", "nsfw", "untagged", "all"]; - -class MediaService extends GetConnect { - final EncryptedSharedPreferencesAsync storage = - EncryptedSharedPreferencesAsync.getInstance(); - - Future> fetchMediaItems({ - required int type, - required int mode, - required int random, - String? tag, - int? older, - }) async { - final String? token = await storage.getString('token'); - final Map 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 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; - 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?> 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 fetchItem(int itemId) async { - final String? token = await storage.getString('token'); - final Map headers = token != null - ? {'Authorization': 'Bearer $token'} - : {}; - - try { - final Response 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; - } - } -} diff --git a/lib/services/api.dart b/lib/services/api.dart new file mode 100644 index 0000000..ddeb79c --- /dev/null +++ b/lib/services/api.dart @@ -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 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 = { + '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 headers = {}; + if (token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + + final Response response = await get( + 'https://api.f0ck.me/items_new/get', + query: params, + headers: headers, + ); + + if (response.status.code == 200 && response.body is Map) { + final Feed feed = Feed.fromJson(response.body as Map); + feed.items.sort((a, b) => b.id.compareTo(a.id)); + return feed; + } else { + throw Exception('Fehler beim Laden der Items'); + } + } + + Future?> toggleFavorite( + MediaItem item, + bool isFavorite, + ) async { + String? token = await storage.getString('token'); + if (token == null || token.isEmpty) return null; + + final Map 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; + } + } +} diff --git a/lib/utils/animatedtransition.dart b/lib/utils/animatedtransition.dart index b3f1072..1f7f9dc 100644 --- a/lib/utils/animatedtransition.dart +++ b/lib/utils/animatedtransition.dart @@ -1,50 +1,50 @@ 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 } Widget buildAnimatedTransition({ - required BuildContext context, - required Widget child, - required PageController pageController, - required int index, - required MediaController controller, - }) { - final double value = pageController.position.haveDimensions - ? pageController.page! - index - : 0; + required BuildContext context, + required Widget child, + required PageController pageController, + required int index, + required MediaController controller, +}) { + final double value = pageController.position.haveDimensions + ? pageController.page! - index + : 0; - switch (controller.transitionType.value) { - case PageTransition.opacity: - return Opacity( - opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)), - child: Transform(transform: Matrix4.identity(), child: child), - ); - case PageTransition.scale: - return Transform.scale( - scale: - 0.8 + - Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)) * 0.2, - child: child, - ); - case PageTransition.slide: - return Transform.translate( - offset: Offset(300 * value.abs(), 0), - child: child, - ); - case PageTransition.rotate: - return Opacity( - opacity: (1 - value.abs()).clamp(0.0, 1.0), - child: Transform.rotate(angle: value.abs() * 0.5, child: child), - ); - case PageTransition.flip: - return Transform( - transform: Matrix4.identity() - ..setEntry(3, 2, 0.001) - ..rotateY(value.abs()), - alignment: Alignment.center, - child: child, - ); - } + switch (controller.transitionType.value) { + case PageTransition.opacity: + return Opacity( + opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)), + child: Transform(transform: Matrix4.identity(), child: child), + ); + case PageTransition.scale: + return Transform.scale( + scale: + 0.8 + + Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)) * 0.2, + child: child, + ); + case PageTransition.slide: + return Transform.translate( + offset: Offset(300 * value.abs(), 0), + child: child, + ); + case PageTransition.rotate: + return Opacity( + opacity: (1 - value.abs()).clamp(0.0, 1.0), + child: Transform.rotate(angle: value.abs() * 0.5, child: child), + ); + case PageTransition.flip: + return Transform( + transform: Matrix4.identity() + ..setEntry(3, 2, 0.001) + ..rotateY(value.abs()), + alignment: Alignment.center, + child: child, + ); } +} diff --git a/lib/utils/customsearchdelegate.dart b/lib/utils/customsearchdelegate.dart index c2021b3..d4e9f6c 100644 --- a/lib/utils/customsearchdelegate.dart +++ b/lib/utils/customsearchdelegate.dart @@ -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 { final MediaController controller = Get.find(); @@ -144,8 +144,8 @@ class CustomSearchDelegate extends SearchDelegate { '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); }, ); diff --git a/lib/widgets/actiontag.dart b/lib/widgets/actiontag.dart index ff746e8..95a82ed 100644 --- a/lib/widgets/actiontag.dart +++ b/lib/widgets/actiontag.dart @@ -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: [ diff --git a/lib/widgets/detailmediacontent.dart b/lib/widgets/detailmediacontent.dart deleted file mode 100644 index fd2d9fd..0000000 --- a/lib/widgets/detailmediacontent.dart +++ /dev/null @@ -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? 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((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, - ), - ], - ); - } -} diff --git a/lib/widgets/end_drawer.dart b/lib/widgets/end_drawer.dart index 3f07e44..ecfb14a 100644 --- a/lib/widgets/end_drawer.dart +++ b/lib/widgets/end_drawer.dart @@ -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'), ), diff --git a/lib/widgets/favorite_avatars.dart b/lib/widgets/favorite_avatars.dart index 10508a2..a18a7c2 100644 --- a/lib/widgets/favorite_avatars.dart +++ b/lib/widgets/favorite_avatars.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'package:cached_network_image/cached_network_image.dart'; class FavoriteAvatars extends StatelessWidget { diff --git a/lib/widgets/filter_bar.dart b/lib/widgets/filter_bar.dart index 18594a5..5121039 100644 --- a/lib/widgets/filter_bar.dart +++ b/lib/widgets/filter_bar.dart @@ -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,39 +19,43 @@ class FilterBar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text('type: '), - Obx(() => DropdownButton( - value: mediaTypes[c.type.value], - isDense: true, - items: mediaTypes.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - c.setType(mediaTypes.indexOf(newValue)); - scrollController.jumpTo(0); - } - }, - )), + Obx( + () => DropdownButton( + value: mediaTypes[c.typeIndex.value], + isDense: true, + items: mediaTypes.map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + c.setTypeIndex(mediaTypes.indexOf(newValue)); + scrollController.jumpTo(0); + } + }, + ), + ), const Text('mode: '), - Obx(() => DropdownButton( - value: mediaModes[c.mode.value], - isDense: true, - items: mediaModes.map((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - c.setMode(mediaModes.indexOf(newValue)); - scrollController.jumpTo(0); - } - }, - )), + Obx( + () => DropdownButton( + value: mediaModes[c.modeIndex.value], + isDense: true, + items: mediaModes.map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + c.setModeIndex(mediaModes.indexOf(newValue)); + scrollController.jumpTo(0); + } + }, + ), + ), ], ), ); diff --git a/lib/widgets/media_tile.dart b/lib/widgets/media_tile.dart index b6c3a72..9d84456 100644 --- a/lib/widgets/media_tile.dart +++ b/lib/widgets/media_tile.dart @@ -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, - fit: BoxFit.cover, - placeholder: (content, url) => Container(color: Colors.grey[900]), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), + CachedNetworkImage( + imageUrl: 'https://f0ck.me/t/${item.id}.webp', + fit: BoxFit.cover, + placeholder: (context, url) => Container(color: Colors.grey[900]), + errorWidget: (context, url, error) => + const Icon(Icons.broken_image), ), Align( alignment: Alignment.bottomRight, diff --git a/lib/widgets/videooverlay_widget.dart b/lib/widgets/video_controls_overlay.dart similarity index 50% rename from lib/widgets/videooverlay_widget.dart rename to lib/widgets/video_controls_overlay.dart index 99eed49..a436277 100644 --- a/lib/widgets/videooverlay_widget.dart +++ b/lib/widgets/video_controls_overlay.dart @@ -1,75 +1,66 @@ 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(); - return Stack( alignment: Alignment.center, children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _ControlButton(Icons.replay_10, () { + onOverlayTap(); + Duration newPosition = + controller.value.position - const Duration(seconds: 10); + if (newPosition < Duration.zero) newPosition = Duration.zero; + controller.seekTo(newPosition); + }), + const SizedBox(width: 40), + _ControlButton( + controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + () { + onOverlayTap(); + controller.value.isPlaying + ? controller.pause() + : controller.play(); + }, + size: 64, + ), + const SizedBox(width: 40), + _ControlButton(Icons.forward_10, () { + onOverlayTap(); + Duration newPosition = + controller.value.position + const Duration(seconds: 10); + if (newPosition > controller.value.duration) { + newPosition = controller.value.duration; + } + controller.seekTo(newPosition); + }), + ], + ), Positioned( 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( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _ControlButton(Icons.replay_10, () { - button(); - Duration newPosition = - controller.value.position - const Duration(seconds: 10); - if (newPosition < Duration.zero) newPosition = Duration.zero; - controller.seekTo(newPosition); - }), - SizedBox(width: 40), - _ControlButton( - controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - () { - button(); - controller.value.isPlaying - ? controller.pause() - : controller.play(); - }, - size: 64, - ), - SizedBox(width: 40), - _ControlButton(Icons.forward_10, () { - button(); - Duration newPosition = - controller.value.position + const Duration(seconds: 10); - if (newPosition > controller.value.duration) { - newPosition = controller.value.duration; - } - controller.seekTo(newPosition); - }), - ], - ), + 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,23 +92,24 @@ class VideoControlsOverlay extends StatelessWidget { ), ), ), - Positioned( - left: - (controller.value.position.inMilliseconds / - controller.value.duration.inMilliseconds) * - MediaQuery.of(context).size.width - - 6, - bottom: -4, - child: Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.red, - border: Border.all(color: Colors.red, width: 2), + if (controller.value.duration.inMilliseconds > 0) + Positioned( + left: + (controller.value.position.inMilliseconds / + controller.value.duration.inMilliseconds) * + MediaQuery.of(context).size.width - + 6, + bottom: -4, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.red, + border: Border.all(color: Colors.red, width: 2), + ), ), ), - ), ], ), ), diff --git a/lib/widgets/video_widget.dart b/lib/widgets/video_widget.dart index 696b478..74bbc77 100644 --- a/lib/widgets/video_widget.dart +++ b/lib/widgets/video_widget.dart @@ -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 { ); 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 { bool isAudio = widget.details.mime.startsWith('audio'); - if (widget.fullScreen) { - return Stack( + Widget mediaContent; + if (isAudio) { + mediaContent = CachedNetworkImage( + imageUrl: widget.details.coverUrl, + fit: BoxFit.cover, + errorWidget: (c, e, s) => Image.asset( + 'assets/images/music.webp', + fit: BoxFit.contain, + width: double.infinity, + ), + ); + } else { + mediaContent = _controller.value.isInitialized + ? CachedVideoPlayerPlus(_controller) + : const Center(child: CircularProgressIndicator()); + } + + return AspectRatio( + aspectRatio: _controller.value.isInitialized + ? _controller.value.aspectRatio + : (isAudio ? 16 / 9 : 9 / 16), + child: Stack( + alignment: Alignment.center, children: [ - Center( - child: AspectRatio( - aspectRatio: _controller.value.isInitialized - ? _controller.value.aspectRatio - : 9 / 16, - child: GestureDetector( - onTap: _onTap, - child: isAudio - ? CachedNetworkImage( - imageUrl: widget.details.coverUrl, - fit: BoxFit.cover, - placeholder: (context, url) => - const CircularProgressIndicator(), - errorWidget: (context, url, error) => Image.asset( - 'assets/images/music.webp', - fit: BoxFit.contain, - width: double.infinity, - ), - ) - : _controller.value.isInitialized - ? CachedVideoPlayerPlus(_controller) - : const Center(child: CircularProgressIndicator()), - ), - ), - ), + GestureDetector(onTap: _onTap, child: mediaContent), if (_controller.value.isInitialized && _showControls) Positioned.fill( child: GestureDetector( @@ -132,61 +127,18 @@ class _VideoWidgetState extends State { 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), - ), - ], - ], - ), - ), - ], - ); - } + ), + ); } } diff --git a/pubspec.lock b/pubspec.lock index 380d18c..b1ab812 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 60a4750..1360ce3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.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: