diff --git a/lib/controller/mediacontroller.dart b/lib/controller/mediacontroller.dart index 6c0d1bc..0dc8f9f 100644 --- a/lib/controller/mediacontroller.dart +++ b/lib/controller/mediacontroller.dart @@ -1,18 +1,14 @@ 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 = Get.find(); - final EncryptedSharedPreferencesAsync storage = - EncryptedSharedPreferencesAsync.getInstance(); RxList items = [].obs; RxBool loading = false.obs; @@ -23,12 +19,6 @@ class MediaController extends GetxController { RxInt modeIndex = 0.obs; RxInt random = 0.obs; Rxn tag = Rxn(null); - RxBool muted = false.obs; - Rx transitionType = PageTransition.opacity.obs; - RxBool drawerSwipeEnabled = true.obs; - RxInt crossAxisCount = 0.obs; - RxInt videoControlsTimerNotifier = 0.obs; - RxInt hideControlsNotifier = 0.obs; void setTypeIndex(int idx) { typeIndex.value = idx; @@ -137,58 +127,10 @@ class MediaController extends GetxController { } } - 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(); } - void resetVideoControlsTimer() => videoControlsTimerNotifier.value++; - void hideVideoControls() => hideControlsNotifier.value++; - - @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); - } - bool get isRandomEnabled => random.value == 1; } diff --git a/lib/controller/settingscontroller.dart b/lib/controller/settingscontroller.dart new file mode 100644 index 0000000..2af45c4 --- /dev/null +++ b/lib/controller/settingscontroller.dart @@ -0,0 +1,81 @@ +import 'package:encrypt_shared_preferences/provider.dart'; +import 'package:get/get.dart'; + +import 'package:f0ckapp/utils/animatedtransition.dart'; + +class _StorageKeys { + static const String muted = 'muted'; + static const String crossAxisCount = 'crossAxisCount'; + static const String drawerSwipeEnabled = 'drawerSwipeEnabled'; + static const String transitionType = 'transitionType'; +} + +class SettingsController extends GetxController { + final EncryptedSharedPreferencesAsync storage = + EncryptedSharedPreferencesAsync.getInstance(); + + RxBool muted = false.obs; + Rx transitionType = PageTransition.opacity.obs; + RxBool drawerSwipeEnabled = true.obs; + RxInt crossAxisCount = 0.obs; + + RxInt videoControlsTimerNotifier = 0.obs; + RxInt hideControlsNotifier = 0.obs; + + @override + void onInit() { + super.onInit(); + loadSettings(); + } + + void toggleMuted() { + muted.value = !muted.value; + saveSettings(); + } + + void setMuted(bool value) { + muted.value = value; + saveSettings(); + } + + 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 resetVideoControlsTimer() => videoControlsTimerNotifier.value++; + void hideVideoControls() => hideControlsNotifier.value++; + + Future loadSettings() async { + muted.value = await storage.getBoolean(_StorageKeys.muted) ?? false; + crossAxisCount.value = + await storage.getInt(_StorageKeys.crossAxisCount) ?? 0; + drawerSwipeEnabled.value = + await storage.getBoolean(_StorageKeys.drawerSwipeEnabled) ?? true; + transitionType.value = PageTransition + .values[await storage.getInt(_StorageKeys.transitionType) ?? 0]; + } + + Future saveSettings() async { + await storage.setBoolean(_StorageKeys.muted, muted.value); + await storage.setInt(_StorageKeys.crossAxisCount, crossAxisCount.value); + await storage.setBoolean( + _StorageKeys.drawerSwipeEnabled, + drawerSwipeEnabled.value, + ); + await storage.setInt( + _StorageKeys.transitionType, + transitionType.value.index, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c60493d..548df06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:encrypt_shared_preferences/provider.dart'; import 'package:get/get.dart'; import 'package:f0ckapp/services/api.dart'; +import 'package:f0ckapp/controller/settingscontroller.dart'; import 'package:f0ckapp/controller/authcontroller.dart'; import 'package:f0ckapp/controller/localizationcontroller.dart'; import 'package:f0ckapp/controller/themecontroller.dart'; @@ -24,6 +25,7 @@ void main() async { Get.put(AuthController()); Get.put(ApiService()); + Get.put(SettingsController()); Get.put(MediaController()); final ThemeController themeController = Get.put(ThemeController()); diff --git a/lib/screens/mediadetail.dart b/lib/screens/mediadetail.dart index 46067ee..5500f13 100644 --- a/lib/screens/mediadetail.dart +++ b/lib/screens/mediadetail.dart @@ -5,17 +5,20 @@ 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:pullex/pullex.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:f0ckapp/services/api.dart'; import 'package:f0ckapp/widgets/tagfooter.dart'; import 'package:f0ckapp/utils/animatedtransition.dart'; import 'package:f0ckapp/controller/authcontroller.dart'; -import 'package:f0ckapp/widgets/actiontag.dart'; import 'package:f0ckapp/widgets/favoritesection.dart'; import 'package:f0ckapp/screens/fullscreen.dart'; import 'package:f0ckapp/widgets/end_drawer.dart'; +import 'package:f0ckapp/controller/settingscontroller.dart'; import 'package:f0ckapp/controller/mediacontroller.dart'; import 'package:f0ckapp/models/item.dart'; +import 'package:f0ckapp/widgets/tagsection.dart'; import 'package:f0ckapp/widgets/video_widget.dart'; enum ShareAction { media, directLink, postLink } @@ -31,10 +34,11 @@ class MediaDetailScreen extends StatefulWidget { class _MediaDetailScreenState extends State { PageController? _pageController; final MediaController mediaController = Get.find(); + final SettingsController settingsController = Get.find(); final AuthController authController = Get.find(); final RxInt _currentIndex = 0.obs; final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit'); - final Map _expandedTags = {}; + final Map _refreshControllers = {}; bool _isLoading = true; bool _itemNotFound = false; @@ -103,6 +107,31 @@ class _MediaDetailScreenState extends State { ..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM); } + Future _onRefresh( + int itemId, + PullexRefreshController controller, + ) async { + if (mediaController.loading.value) { + controller.refreshCompleted(); + return; + } + + try { + final MediaItem item = await ApiService().fetchItemById(itemId); + final int index = mediaController.items.indexWhere( + (item) => item.id == itemId, + ); + if (index != -1) { + mediaController.items[index] = item; + mediaController.items.refresh(); + } + controller.refreshCompleted(); + } catch (e) { + _showMsg('Fehler beim Aktualisieren: $e'); + controller.refreshFailed(); + } + } + void _onPageChanged(int idx) { if (idx != _currentIndex.value) { _currentIndex.value = idx; @@ -148,7 +177,7 @@ class _MediaDetailScreenState extends State { item.mediaUrl, ); final Uint8List bytes = await file.readAsBytes(); - final params = ShareParams( + final ShareParams params = ShareParams( files: [XFile.fromData(bytes, mimeType: item.mime)], ); await SharePlus.instance.share(params); @@ -175,6 +204,9 @@ class _MediaDetailScreenState extends State { @override void dispose() { _pageController?.dispose(); + for (PullexRefreshController controller in _refreshControllers.values) { + controller.dispose(); + } super.dispose(); } @@ -217,152 +249,125 @@ class _MediaDetailScreenState extends State { ); } - return Obx( - () => PageView.builder( - controller: _pageController!, - itemCount: mediaController.items.length, - onPageChanged: _onPageChanged, - itemBuilder: (context, index) { - final MediaItem item = mediaController.items[index]; - final bool isReady = _readyItemIds.contains(item.id); - final bool areTagsExpanded = _expandedTags[item.id] ?? false; - final List allTags = item.tags ?? []; - final bool hasMoreTags = allTags.length > 5; - final List tagsToShow = areTagsExpanded - ? allTags - : allTags.take(5).toList(); - - return Obx( - () => Scaffold( - endDrawer: EndDrawer(), - endDrawerEnableOpenDragGesture: - mediaController.drawerSwipeEnabled.value, - appBar: AppBar( - 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) => _handleShareAction(value, item), - itemBuilder: (context) => _shareMenuItems, - icon: const Icon(Icons.share), - ), - Builder( - builder: (context) => IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - Scaffold.of(context).openEndDrawer(); - }, - ), - ), - ], + return Obx(() { + if (mediaController.items.isEmpty || + _currentIndex.value >= mediaController.items.length) { + return Scaffold( + appBar: AppBar(title: const Text('Fehler')), + body: const Center(child: Text('Keine Items zum Anzeigen.')), + ); + } + final MediaItem currentItem = mediaController.items[_currentIndex.value]; + return Scaffold( + endDrawer: const EndDrawer(), + endDrawerEnableOpenDragGesture: + settingsController.drawerSwipeEnabled.value, + appBar: AppBar( + title: Text('f0ck #${currentItem.id}'), + 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) => _handleShareAction(value, currentItem), + itemBuilder: (context) => _shareMenuItems, + icon: const Icon(Icons.share), + ), + Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.menu), + onPressed: () => Scaffold.of(context).openEndDrawer(), ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - AnimatedBuilder( - animation: _pageController!, - builder: (context, child) { - return buildAnimatedTransition( - context: context, - pageController: _pageController!, - index: index, - controller: mediaController, - child: child!, - ); - }, - child: Obx( - () => _buildMedia(item, index == _currentIndex.value), + ), + ], + ), + body: PageView.builder( + controller: _pageController!, + itemCount: mediaController.items.length, + onPageChanged: _onPageChanged, + itemBuilder: (context, index) { + final MediaItem item = mediaController.items[index]; + final bool isReady = _readyItemIds.contains(item.id); + final ScrollController scrollController = ScrollController(); + final PullexRefreshController refreshController = + _refreshControllers.putIfAbsent( + item.id, + () => PullexRefreshController(), + ); + + return PullexRefresh( + onRefresh: () => _onRefresh(item.id, refreshController), + header: const WaterDropHeader(), + controller: refreshController, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: AnimatedBuilder( + animation: _pageController!, + builder: (context, child) { + return buildAnimatedTransition( + context: context, + pageController: _pageController!, + index: index, + child: child!, + ); + }, + child: Obx( + () => _buildMedia(item, index == _currentIndex.value), + ), ), ), - Expanded( - child: GestureDetector( - onTap: () => mediaController.hideVideoControls(), - behavior: HitTestBehavior.translucent, - child: Visibility( - visible: isReady, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Wrap( - spacing: 6.0, - runSpacing: 4.0, - alignment: WrapAlignment.center, - children: [ - ...tagsToShow.map( - (tag) => ActionTag( - tag, - (tag.tag == 'sfw' || tag.tag == 'nsfw') - ? (onTagTap) => {} - : (onTagTap) { - mediaController.setTag( - onTagTap, - ); - Get.offAllNamed('/'); - }, - ), - ), - ], - ), - if (hasMoreTags) - TextButton( - onPressed: () { - setState( - () => _expandedTags[item.id] = - !areTagsExpanded, - ); - }, - child: Text( - areTagsExpanded - ? 'Weniger anzeigen' - : 'Alle ${allTags.length} Tags anzeigen', - ), - ), - Obx( - () => Visibility( - visible: authController.isLoggedIn, - child: Padding( - padding: const EdgeInsets.only(top: 20.0), - child: FavoriteSection( - item: item, - index: index, - ), + if (isReady) + SliverToBoxAdapter( + child: GestureDetector( + onTap: () => settingsController.hideVideoControls(), + behavior: HitTestBehavior.translucent, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TagSection(tags: item.tags ?? []), + Obx( + () => Visibility( + visible: authController.isLoggedIn, + child: Padding( + padding: const EdgeInsets.only(top: 20.0), + child: FavoriteSection( + item: item, + index: index, ), ), ), - ], - ), + ), + ], ), ), ), ), + const SliverToBoxAdapter( + child: SafeArea(child: SizedBox.shrink()), ), - const SafeArea(child: SizedBox.shrink()), ], ), - persistentFooterButtons: mediaController.tag.value != null - ? [TagFooter()] - : null, - ), - ); - }, - ), - ); + ); + }, + ), + persistentFooterButtons: mediaController.tag.value != null + ? [TagFooter()] + : null, + ); + }); } } diff --git a/lib/screens/mediagrid.dart b/lib/screens/mediagrid.dart index 6874e13..6ec2f28 100644 --- a/lib/screens/mediagrid.dart +++ b/lib/screens/mediagrid.dart @@ -8,6 +8,7 @@ import 'package:f0ckapp/utils/customsearchdelegate.dart'; import 'package:f0ckapp/widgets/end_drawer.dart'; import 'package:f0ckapp/widgets/filter_bar.dart'; import 'package:f0ckapp/widgets/media_tile.dart'; +import 'package:f0ckapp/controller/settingscontroller.dart'; import 'package:f0ckapp/controller/mediacontroller.dart'; class MediaGrid extends StatefulWidget { @@ -20,6 +21,7 @@ class MediaGrid extends StatefulWidget { class _MediaGrid extends State { final ScrollController _scrollController = ScrollController(); final MediaController _mediaController = Get.put(MediaController()); + final SettingsController _settingsController = Get.put(SettingsController()); final PullexRefreshController _refreshController = PullexRefreshController( initialRefresh: false, ); @@ -35,6 +37,7 @@ class _MediaGrid extends State { _body = _MediaGridBody( refreshController: _refreshController, mediaController: _mediaController, + settingsController: _settingsController, scrollController: _scrollController, ); } @@ -52,7 +55,7 @@ class _MediaGrid extends State { () => Scaffold( endDrawer: const EndDrawer(), endDrawerEnableOpenDragGesture: - _mediaController.drawerSwipeEnabled.value, + _settingsController.drawerSwipeEnabled.value, bottomNavigationBar: FilterBar(scrollController: _scrollController), appBar: _appBar, body: _body, @@ -121,11 +124,13 @@ class _MediaGridBody extends StatelessWidget { const _MediaGridBody({ required this.refreshController, required this.mediaController, + required this.settingsController, required this.scrollController, }); final PullexRefreshController refreshController; final MediaController mediaController; + final SettingsController settingsController; final ScrollController scrollController; @override @@ -134,7 +139,7 @@ class _MediaGridBody extends StatelessWidget { controller: refreshController, enablePullDown: true, enablePullUp: true, - header: const MaterialHeader(), + header: const WaterDropHeader(), onRefresh: () async { try { await mediaController.handleRefresh(); @@ -157,7 +162,7 @@ class _MediaGridBody extends StatelessWidget { shrinkWrap: true, padding: const EdgeInsets.all(4), itemCount: mediaController.items.length, - gridDelegate: mediaController.crossAxisCount.value == 0 + gridDelegate: settingsController.crossAxisCount.value == 0 ? const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150, crossAxisSpacing: 5, @@ -165,7 +170,7 @@ class _MediaGridBody extends StatelessWidget { childAspectRatio: 1, ) : SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: mediaController.crossAxisCount.value, + crossAxisCount: settingsController.crossAxisCount.value, crossAxisSpacing: 5, mainAxisSpacing: 5, childAspectRatio: 1, diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index d510c97..a35fbda 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -4,7 +4,7 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:get/get.dart'; import 'package:f0ckapp/controller/localizationcontroller.dart'; -import 'package:f0ckapp/controller/mediacontroller.dart'; +import 'package:f0ckapp/controller/settingscontroller.dart'; import 'package:f0ckapp/utils/animatedtransition.dart'; class SettingsPage extends StatefulWidget { @@ -15,8 +15,8 @@ class SettingsPage extends StatefulWidget { } class _SettingsPageState extends State { - final MediaController controller = Get.find(); - final LocalizationController localizationController = Get.find(); + final SettingsController settingsController = Get.find(); + final LocalizationController localizationController = Get.find(); @override Widget build(BuildContext context) { @@ -34,7 +34,7 @@ class _SettingsPageState extends State { title: Text('settings_numberofcolumns_title'.tr), trailing: Obx( () => DropdownButton( - value: controller.crossAxisCount.value, + value: settingsController.crossAxisCount.value, dropdownColor: const Color.fromARGB(255, 43, 43, 43), iconEnabledColor: Colors.white, items: [0, 3, 4, 5].map((int value) { @@ -51,7 +51,7 @@ class _SettingsPageState extends State { }).toList(), onChanged: (int? newValue) async { if (newValue != null) { - await controller.setCrossAxisCount(newValue); + await settingsController.setCrossAxisCount(newValue); } }, ), @@ -62,7 +62,7 @@ class _SettingsPageState extends State { title: Text('settings_pageanimation_title'.tr), trailing: Obx( () => DropdownButton( - value: controller.transitionType.value, + value: settingsController.transitionType.value, dropdownColor: const Color.fromARGB(255, 43, 43, 43), iconEnabledColor: Colors.white, items: PageTransition.values.map((PageTransition type) { @@ -91,7 +91,7 @@ class _SettingsPageState extends State { }).toList(), onChanged: (PageTransition? newValue) async { if (newValue != null) { - await controller.setTransitionType(newValue); + await settingsController.setTransitionType(newValue); } }, ), @@ -102,9 +102,9 @@ class _SettingsPageState extends State { SwitchListTile( title: Text('settings_drawer_title'.tr), subtitle: Text('settings_drawer_subtitle'.tr), - value: controller.drawerSwipeEnabled.value, + value: settingsController.drawerSwipeEnabled.value, onChanged: (bool value) async { - await controller.setDrawerSwipeEnabled(value); + await settingsController.setDrawerSwipeEnabled(value); setState(() {}); }, ), diff --git a/lib/services/api.dart b/lib/services/api.dart index 2f2370d..78529e0 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -1,12 +1,11 @@ -import 'package:encrypt_shared_preferences/provider.dart'; import 'package:get/get.dart'; +import 'package:f0ckapp/controller/authcontroller.dart'; import 'package:f0ckapp/models/item.dart'; import 'package:f0ckapp/models/feed.dart'; class ApiService extends GetConnect { - final EncryptedSharedPreferencesAsync storage = - EncryptedSharedPreferencesAsync.getInstance(); + final AuthController _authController = Get.find(); Future fetchItems({ int? older, @@ -16,7 +15,7 @@ class ApiService extends GetConnect { int random = 0, String? tag, }) async { - String? token = await storage.getString('token'); + String? token = _authController.token.value; final params = { 'type': type.toString(), 'mode': mode.toString(), @@ -42,18 +41,40 @@ class ApiService extends GetConnect { feed.items.sort((a, b) => b.id.compareTo(a.id)); return feed; } else { - if (Get.isSnackbarOpen == false) { + if (!Get.isSnackbarOpen) { Get.snackbar('Fehler', 'Fehler beim Laden der Items'); } throw Exception('Fehler beim Laden der Items'); } } + Future fetchItemById(int itemId) async { + String? token = _authController.token.value; + final Map headers = {}; + if (token != null && token.isNotEmpty) { + headers['Authorization'] = 'Bearer $token'; + } + + 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 as Map); + } else { + if (!Get.isSnackbarOpen) { + Get.snackbar('Fehler', 'Fehler beim Laden des Items'); + } + throw Exception('Fehler beim Laden des Items'); + } + } + Future?> toggleFavorite( MediaItem item, bool isFavorite, ) async { - String? token = await storage.getString('token'); + String? token = _authController.token.value; if (token == null || token.isEmpty) return null; final Map headers = { diff --git a/lib/utils/animatedtransition.dart b/lib/utils/animatedtransition.dart index 1f7f9dc..83d1189 100644 --- a/lib/utils/animatedtransition.dart +++ b/lib/utils/animatedtransition.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:f0ckapp/controller/mediacontroller.dart'; +import 'package:get/get.dart'; + +import 'package:f0ckapp/controller/settingscontroller.dart'; enum PageTransition { opacity, scale, slide, rotate, flip } @@ -9,17 +11,17 @@ Widget buildAnimatedTransition({ required Widget child, required PageController pageController, required int index, - required MediaController controller, }) { + final SettingsController settingsController = Get.find(); final double value = pageController.position.haveDimensions ? pageController.page! - index : 0; - switch (controller.transitionType.value) { + switch (settingsController.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), + child: child, ); case PageTransition.scale: return Transform.scale( @@ -29,10 +31,7 @@ Widget buildAnimatedTransition({ child: child, ); case PageTransition.slide: - return Transform.translate( - offset: Offset(300 * value.abs(), 0), - child: child, - ); + return child; case PageTransition.rotate: return Opacity( opacity: (1 - value.abs()).clamp(0.0, 1.0), diff --git a/lib/widgets/media_tile.dart b/lib/widgets/media_tile.dart index 836df5d..4dd4dc3 100644 --- a/lib/widgets/media_tile.dart +++ b/lib/widgets/media_tile.dart @@ -16,7 +16,7 @@ class MediaTile extends StatelessWidget { fit: StackFit.expand, children: [ CachedNetworkImage( - imageUrl: 'https://f0ck.me/t/${item.id}.webp', + imageUrl: item.thumbnailUrl, fit: BoxFit.cover, placeholder: (context, url) => Container(color: Colors.grey[900]), errorWidget: (context, url, error) => diff --git a/lib/widgets/tagsection.dart b/lib/widgets/tagsection.dart new file mode 100644 index 0000000..dd2a27d --- /dev/null +++ b/lib/widgets/tagsection.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import 'package:get/get.dart'; + +import 'package:f0ckapp/models/item.dart'; +import 'package:f0ckapp/widgets/actiontag.dart'; +import 'package:f0ckapp/controller/mediacontroller.dart'; + +class TagSection extends StatefulWidget { + final List tags; + const TagSection({super.key, required this.tags}); + + @override + State createState() => _TagSectionState(); +} + +class _TagSectionState extends State { + bool _areTagsExpanded = false; + + @override + Widget build(BuildContext context) { + final MediaController mediaController = Get.find(); + final bool hasMoreTags = widget.tags.length > 5; + final List tagsToShow = _areTagsExpanded + ? widget.tags + : widget.tags.take(5).toList(); + + return Column( + children: [ + Wrap( + spacing: 6.0, + runSpacing: 4.0, + alignment: WrapAlignment.center, + children: [ + ...tagsToShow.map( + (tag) => ActionTag( + tag, + (tag.tag == 'sfw' || tag.tag == 'nsfw') + ? (onTagTap) => {} + : (onTagTap) { + mediaController.setTag(onTagTap); + Get.offAllNamed('/'); + }, + ), + ), + ], + ), + if (hasMoreTags) + TextButton( + onPressed: () { + setState(() => _areTagsExpanded = !_areTagsExpanded); + }, + child: Text( + _areTagsExpanded + ? 'Weniger anzeigen' + : 'Alle ${widget.tags.length} Tags anzeigen', + ), + ), + ], + ); + } +} diff --git a/lib/widgets/video_controls_overlay.dart b/lib/widgets/video_controls_overlay.dart index 6cbc47d..1d395b2 100644 --- a/lib/widgets/video_controls_overlay.dart +++ b/lib/widgets/video_controls_overlay.dart @@ -94,81 +94,99 @@ class _VideoControlsOverlayState extends State { ), ), ), - _ControlButton( - widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow, - () { + IconButton( + icon: Icon( + widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + size: 64, + ), + onPressed: () { widget.onOverlayTap(); widget.controller.value.isPlaying ? widget.controller.pause() : widget.controller.play(); }, - size: 64, ), Positioned( right: 12, bottom: 12, - child: _ControlButton( - widget.muted ? Icons.volume_off : Icons.volume_up, - () { + child: IconButton( + icon: Icon( + widget.muted ? Icons.volume_off : Icons.volume_up, + size: 16, + ), + onPressed: () { widget.onOverlayTap(); widget.onMuteToggle(); }, - size: 16, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 0), - child: Stack( - clipBehavior: Clip.none, - children: [ - Positioned( - left: 10, - bottom: 12, - child: Text( - '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - ), - Listener( - onPointerDown: (_) { - widget.onOverlayTap(); - }, - child: VideoProgressIndicator( - widget.controller, - allowScrubbing: true, - padding: const EdgeInsets.only(top: 25.0), - colors: const VideoProgressColors( - playedColor: Colors.red, - backgroundColor: Colors.grey, - bufferedColor: Colors.white54, - ), - ), - ), - if (widget.controller.value.duration.inMilliseconds > 0) - Positioned( - left: - (widget.controller.value.position.inMilliseconds / - widget - .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), + child: LayoutBuilder( + builder: (context, constraints) { + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + left: 10, + bottom: 12, + child: Text( + '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), ), ), - ), - ], + Listener( + onPointerDown: (_) { + widget.onOverlayTap(); + }, + child: VideoProgressIndicator( + widget.controller, + allowScrubbing: true, + padding: const EdgeInsets.only(top: 25.0), + colors: VideoProgressColors( + playedColor: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.5), + bufferedColor: Theme.of( + context, + ).colorScheme.secondary.withValues(alpha: 0.5), + ), + ), + ), + if (widget.controller.value.duration.inMilliseconds > 0) + Positioned( + left: + (widget.controller.value.position.inMilliseconds / + widget + .controller + .value + .duration + .inMilliseconds) * + constraints.maxWidth - + 6, + bottom: -4, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary, + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + ), + ), + ], + ); + }, ), ), ), @@ -182,25 +200,3 @@ class _VideoControlsOverlayState extends State { return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}"; } } - -class _ControlButton extends StatelessWidget { - final IconData icon; - final VoidCallback onPressed; - final double size; - - const _ControlButton(this.icon, this.onPressed, {this.size = 24}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.black.withValues(alpha: 0.4), - ), - child: IconButton( - icon: Icon(icon, size: size), - onPressed: onPressed, - ), - ); - } -} diff --git a/lib/widgets/video_widget.dart b/lib/widgets/video_widget.dart index f5f63e4..507899d 100644 --- a/lib/widgets/video_widget.dart +++ b/lib/widgets/video_widget.dart @@ -8,6 +8,7 @@ import 'package:get/get.dart'; import 'package:f0ckapp/models/item.dart'; import 'package:f0ckapp/widgets/video_controls_overlay.dart'; +import 'package:f0ckapp/controller/settingscontroller.dart'; import 'package:f0ckapp/controller/mediacontroller.dart'; class VideoWidget extends StatefulWidget { @@ -29,8 +30,9 @@ class VideoWidget extends StatefulWidget { } class _VideoWidgetState extends State { - final MediaController controller = Get.find(); - late CachedVideoPlayerPlusController _controller; + final MediaController mediaController = Get.find(); + final SettingsController settingsController = Get.find(); + late CachedVideoPlayerPlusController videoController; late Worker _muteWorker; late Worker _timerResetWorker; late Worker _hideControlsWorker; @@ -41,12 +43,14 @@ class _VideoWidgetState extends State { void initState() { super.initState(); _initController(); - _muteWorker = ever(controller.muted, (bool muted) { - if (_controller.value.isInitialized) { - _controller.setVolume(muted ? 0.0 : 1.0); + _muteWorker = ever(settingsController.muted, (bool muted) { + if (videoController.value.isInitialized) { + videoController.setVolume(muted ? 0.0 : 1.0); } }); - _timerResetWorker = ever(controller.videoControlsTimerNotifier, (_) { + _timerResetWorker = ever(settingsController.videoControlsTimerNotifier, ( + _, + ) { if (widget.isActive && mounted) { if (!_showControls) { setState(() => _showControls = true); @@ -54,7 +58,7 @@ class _VideoWidgetState extends State { _startHideControlsTimer(); } }); - _hideControlsWorker = ever(controller.hideControlsNotifier, (_) { + _hideControlsWorker = ever(settingsController.hideControlsNotifier, (_) { if (mounted && _showControls) { setState(() => _showControls = false); _hideControlsTimer?.cancel(); @@ -63,20 +67,18 @@ class _VideoWidgetState extends State { } Future _initController() async { - _controller = CachedVideoPlayerPlusController.networkUrl( + videoController = CachedVideoPlayerPlusController.networkUrl( Uri.parse(widget.details.mediaUrl), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); - await _controller.initialize(); + await videoController.initialize(); widget.onInitialized?.call(); if (!mounted) return; - setState(() {}); - _controller.addListener(() => setState(() {})); - _controller.setLooping(true); - _controller.setVolume(controller.muted.value ? 0.0 : 1.0); + videoController.setLooping(true); + videoController.setVolume(settingsController.muted.value ? 0.0 : 1.0); if (widget.isActive) { - _controller.play(); + videoController.play(); } } @@ -85,9 +87,9 @@ class _VideoWidgetState extends State { super.didUpdateWidget(oldWidget); if (widget.isActive != oldWidget.isActive) { if (widget.isActive) { - _controller.play(); + videoController.play(); } else { - _controller.pause(); + videoController.pause(); } } } @@ -97,7 +99,7 @@ class _VideoWidgetState extends State { _muteWorker.dispose(); _timerResetWorker.dispose(); _hideControlsWorker.dispose(); - _controller.dispose(); + videoController.dispose(); _hideControlsTimer?.cancel(); super.dispose(); } @@ -129,7 +131,7 @@ class _VideoWidgetState extends State { @override Widget build(BuildContext context) { - final bool muted = controller.muted.value; + final bool muted = settingsController.muted.value; bool isAudio = widget.details.mime.startsWith('audio'); Widget mediaContent; @@ -144,36 +146,43 @@ class _VideoWidgetState extends State { ), ); } else { - mediaContent = _controller.value.isInitialized - ? CachedVideoPlayerPlus(_controller) + mediaContent = videoController.value.isInitialized + ? CachedVideoPlayerPlus(videoController) : const Center(child: CircularProgressIndicator()); } return AspectRatio( - aspectRatio: _controller.value.isInitialized - ? _controller.value.aspectRatio + aspectRatio: videoController.value.isInitialized + ? videoController.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( - onTap: _onTap, - child: Container( - color: Colors.black.withValues(alpha: 0.5), - child: VideoControlsOverlay( - controller: _controller, - onOverlayTap: () => _onTap(ctrlButton: true), - muted: muted, - onMuteToggle: () { - controller.toggleMuted(); - }, + AnimatedBuilder( + animation: videoController, + builder: (context, child) { + if (videoController.value.isInitialized && _showControls) { + return Positioned.fill( + child: GestureDetector( + onTap: _onTap, + child: Container( + color: Colors.black.withValues(alpha: 0.5), + child: VideoControlsOverlay( + controller: videoController, + onOverlayTap: () => _onTap(ctrlButton: true), + muted: muted, + onMuteToggle: () { + settingsController.toggleMuted(); + }, + ), + ), ), - ), - ), - ), + ); + } + return const SizedBox.shrink(); + }, + ), ], ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 8fbb70f..479fa2a 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.4.3+64 +version: 1.4.4+65 environment: sdk: ^3.9.0-100.2.beta