This commit is contained in:
		@@ -4,14 +4,14 @@ import 'package:get/get.dart';
 | 
				
			|||||||
import 'package:encrypt_shared_preferences/provider.dart';
 | 
					import 'package:encrypt_shared_preferences/provider.dart';
 | 
				
			||||||
import 'package:http/http.dart' as http;
 | 
					import 'package:http/http.dart' as http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:f0ckapp/models/user.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthController extends GetxController {
 | 
					class AuthController extends GetxController {
 | 
				
			||||||
  final EncryptedSharedPreferencesAsync storage =
 | 
					  final EncryptedSharedPreferencesAsync storage =
 | 
				
			||||||
      EncryptedSharedPreferencesAsync.getInstance();
 | 
					      EncryptedSharedPreferencesAsync.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  RxnString token = RxnString();
 | 
					  RxnString token = RxnString();
 | 
				
			||||||
  RxnInt userId = RxnInt();
 | 
					  Rxn<User> user = Rxn<User>();
 | 
				
			||||||
  RxnString avatarUrl = RxnString();
 | 
					 | 
				
			||||||
  RxnString username = RxnString();
 | 
					 | 
				
			||||||
  RxBool isLoading = false.obs;
 | 
					  RxBool isLoading = false.obs;
 | 
				
			||||||
  RxnString error = RxnString();
 | 
					  RxnString error = RxnString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,9 +47,7 @@ class AuthController extends GetxController {
 | 
				
			|||||||
      } catch (_) {}
 | 
					      } catch (_) {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    token.value = null;
 | 
					    token.value = null;
 | 
				
			||||||
    userId.value = null;
 | 
					    user.value = null;
 | 
				
			||||||
    avatarUrl.value = null;
 | 
					 | 
				
			||||||
    username.value = null;
 | 
					 | 
				
			||||||
    await storage.remove('token');
 | 
					    await storage.remove('token');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,11 +64,7 @@ class AuthController extends GetxController {
 | 
				
			|||||||
        final dynamic data = json.decode(response.body);
 | 
					        final dynamic data = json.decode(response.body);
 | 
				
			||||||
        if (data['token'] != null) {
 | 
					        if (data['token'] != null) {
 | 
				
			||||||
          await saveToken(data['token']);
 | 
					          await saveToken(data['token']);
 | 
				
			||||||
          userId.value = data['userid'];
 | 
					          user.value = User.fromJson(data);
 | 
				
			||||||
          avatarUrl.value = data['avatar'] != null
 | 
					 | 
				
			||||||
              ? 'https://f0ck.me/t/${data['avatar']}.webp'
 | 
					 | 
				
			||||||
              : null;
 | 
					 | 
				
			||||||
          this.username.value = data['user'];
 | 
					 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          error.value = 'Kein Token erhalten';
 | 
					          error.value = 'Kein Token erhalten';
 | 
				
			||||||
@@ -95,13 +89,7 @@ class AuthController extends GetxController {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
      if (response.statusCode == 200) {
 | 
					      if (response.statusCode == 200) {
 | 
				
			||||||
        final dynamic data = json.decode(response.body);
 | 
					        final dynamic data = json.decode(response.body);
 | 
				
			||||||
        userId.value = data['userid'] != null
 | 
					        user.value = User.fromJson(data);
 | 
				
			||||||
            ? int.tryParse(data['userid'].toString())
 | 
					 | 
				
			||||||
            : null;
 | 
					 | 
				
			||||||
        avatarUrl.value = data['avatar'] != null
 | 
					 | 
				
			||||||
            ? 'https://f0ck.me/t/${data['avatar']}.webp'
 | 
					 | 
				
			||||||
            : null;
 | 
					 | 
				
			||||||
        username.value = data['user'];
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        await logout();
 | 
					        await logout();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,8 @@ class MediaController extends GetxController {
 | 
				
			|||||||
  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
					  Rx<PageTransition> transitionType = PageTransition.opacity.obs;
 | 
				
			||||||
  RxBool drawerSwipeEnabled = true.obs;
 | 
					  RxBool drawerSwipeEnabled = true.obs;
 | 
				
			||||||
  RxInt crossAxisCount = 0.obs;
 | 
					  RxInt crossAxisCount = 0.obs;
 | 
				
			||||||
 | 
					  RxInt videoControlsTimerNotifier = 0.obs;
 | 
				
			||||||
 | 
					  RxInt hideControlsNotifier = 0.obs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setTypeIndex(int idx) {
 | 
					  void setTypeIndex(int idx) {
 | 
				
			||||||
    typeIndex.value = idx;
 | 
					    typeIndex.value = idx;
 | 
				
			||||||
@@ -163,6 +165,9 @@ class MediaController extends GetxController {
 | 
				
			|||||||
    fetchInitial();
 | 
					    fetchInitial();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void resetVideoControlsTimer() => videoControlsTimerNotifier.value++;
 | 
				
			||||||
 | 
					  void hideVideoControls() => hideControlsNotifier.value++;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void onInit() async {
 | 
					  void onInit() async {
 | 
				
			||||||
    super.onInit();
 | 
					    super.onInit();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -186,7 +186,6 @@ final ThemeData f0ck95Theme = ThemeData(
 | 
				
			|||||||
    backgroundColor: const Color(0xFFE0E0E0),
 | 
					    backgroundColor: const Color(0xFFE0E0E0),
 | 
				
			||||||
    foregroundColor: Colors.black,
 | 
					    foregroundColor: Colors.black,
 | 
				
			||||||
    elevation: 4,
 | 
					    elevation: 4,
 | 
				
			||||||
    centerTitle: true,
 | 
					 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  textTheme: const TextTheme(
 | 
					  textTheme: const TextTheme(
 | 
				
			||||||
    bodyLarge: TextStyle(color: Colors.black),
 | 
					    bodyLarge: TextStyle(color: Colors.black),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								lib/models/user.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/models/user.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					class User {
 | 
				
			||||||
 | 
					  final int id;
 | 
				
			||||||
 | 
					  final String username;
 | 
				
			||||||
 | 
					  final String? avatarUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  User({required this.id, required this.username, this.avatarUrl});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory User.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
 | 
					    return User(
 | 
				
			||||||
 | 
					      id: json['userid'],
 | 
				
			||||||
 | 
					      username: json['user'],
 | 
				
			||||||
 | 
					      avatarUrl: json['avatar'] != null
 | 
				
			||||||
 | 
					          ? 'https://f0ck.me/t/${json['avatar']}.webp'
 | 
				
			||||||
 | 
					          : null,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -32,8 +32,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
  PageController? _pageController;
 | 
					  PageController? _pageController;
 | 
				
			||||||
  final MediaController mediaController = Get.find<MediaController>();
 | 
					  final MediaController mediaController = Get.find<MediaController>();
 | 
				
			||||||
  final AuthController authController = Get.find<AuthController>();
 | 
					  final AuthController authController = Get.find<AuthController>();
 | 
				
			||||||
  final _currentIndex = 0.obs;
 | 
					  final RxInt _currentIndex = 0.obs;
 | 
				
			||||||
  final _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
					  final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit');
 | 
				
			||||||
 | 
					  final Map<int, bool> _expandedTags = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _isLoading = true;
 | 
					  bool _isLoading = true;
 | 
				
			||||||
  bool _itemNotFound = false;
 | 
					  bool _itemNotFound = false;
 | 
				
			||||||
@@ -105,7 +106,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
  void _onPageChanged(int idx) {
 | 
					  void _onPageChanged(int idx) {
 | 
				
			||||||
    if (idx != _currentIndex.value) {
 | 
					    if (idx != _currentIndex.value) {
 | 
				
			||||||
      _currentIndex.value = idx;
 | 
					      _currentIndex.value = idx;
 | 
				
			||||||
      final item = mediaController.items[idx];
 | 
					      final MediaItem item = mediaController.items[idx];
 | 
				
			||||||
      if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
 | 
					      if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
 | 
				
			||||||
        setState(() => _readyItemIds.add(item.id));
 | 
					        setState(() => _readyItemIds.add(item.id));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -224,46 +225,52 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
        itemBuilder: (context, index) {
 | 
					        itemBuilder: (context, index) {
 | 
				
			||||||
          final MediaItem item = mediaController.items[index];
 | 
					          final MediaItem item = mediaController.items[index];
 | 
				
			||||||
          final bool isReady = _readyItemIds.contains(item.id);
 | 
					          final bool isReady = _readyItemIds.contains(item.id);
 | 
				
			||||||
 | 
					          final bool areTagsExpanded = _expandedTags[item.id] ?? false;
 | 
				
			||||||
 | 
					          final List<Tag> allTags = item.tags ?? [];
 | 
				
			||||||
 | 
					          final bool hasMoreTags = allTags.length > 5;
 | 
				
			||||||
 | 
					          final List<Tag> tagsToShow = areTagsExpanded
 | 
				
			||||||
 | 
					              ? allTags
 | 
				
			||||||
 | 
					              : allTags.take(5).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return Scaffold(
 | 
					          return Obx(
 | 
				
			||||||
            endDrawer: EndDrawer(),
 | 
					            () => Scaffold(
 | 
				
			||||||
            endDrawerEnableOpenDragGesture:
 | 
					              endDrawer: EndDrawer(),
 | 
				
			||||||
                mediaController.drawerSwipeEnabled.value,
 | 
					              endDrawerEnableOpenDragGesture:
 | 
				
			||||||
            appBar: AppBar(
 | 
					                  mediaController.drawerSwipeEnabled.value,
 | 
				
			||||||
              title: Text('f0ck #${item.id}'),
 | 
					              appBar: AppBar(
 | 
				
			||||||
              actions: [
 | 
					                title: Text('f0ck #${item.id}'),
 | 
				
			||||||
                IconButton(
 | 
					                actions: [
 | 
				
			||||||
                  icon: const Icon(Icons.fullscreen),
 | 
					                  IconButton(
 | 
				
			||||||
                  onPressed: () {
 | 
					                    icon: const Icon(Icons.fullscreen),
 | 
				
			||||||
                    Get.to(
 | 
					 | 
				
			||||||
                      FullScreenMediaView(item: item),
 | 
					 | 
				
			||||||
                      fullscreenDialog: true,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                IconButton(
 | 
					 | 
				
			||||||
                  icon: const Icon(Icons.download),
 | 
					 | 
				
			||||||
                  onPressed: () async {
 | 
					 | 
				
			||||||
                    await _downloadMedia(item);
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                PopupMenuButton<ShareAction>(
 | 
					 | 
				
			||||||
                  onSelected: (value) => _handleShareAction(value, item),
 | 
					 | 
				
			||||||
                  itemBuilder: (context) => _shareMenuItems,
 | 
					 | 
				
			||||||
                  icon: const Icon(Icons.share),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                Builder(
 | 
					 | 
				
			||||||
                  builder: (context) => IconButton(
 | 
					 | 
				
			||||||
                    icon: const Icon(Icons.menu),
 | 
					 | 
				
			||||||
                    onPressed: () {
 | 
					                    onPressed: () {
 | 
				
			||||||
                      Scaffold.of(context).openEndDrawer();
 | 
					                      Get.to(
 | 
				
			||||||
 | 
					                        FullScreenMediaView(item: item),
 | 
				
			||||||
 | 
					                        fullscreenDialog: true,
 | 
				
			||||||
 | 
					                      );
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                  IconButton(
 | 
				
			||||||
              ],
 | 
					                    icon: const Icon(Icons.download),
 | 
				
			||||||
            ),
 | 
					                    onPressed: () async {
 | 
				
			||||||
            body: SingleChildScrollView(
 | 
					                      await _downloadMedia(item);
 | 
				
			||||||
              child: Column(
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  PopupMenuButton<ShareAction>(
 | 
				
			||||||
 | 
					                    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();
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              body: Column(
 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
					                crossAxisAlignment: CrossAxisAlignment.stretch,
 | 
				
			||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  AnimatedBuilder(
 | 
					                  AnimatedBuilder(
 | 
				
			||||||
@@ -281,55 +288,78 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
 | 
				
			|||||||
                      () => _buildMedia(item, index == _currentIndex.value),
 | 
					                      () => _buildMedia(item, index == _currentIndex.value),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const SizedBox(height: 16),
 | 
					                  Expanded(
 | 
				
			||||||
                  if (isReady)
 | 
					                    child: GestureDetector(
 | 
				
			||||||
                    Padding(
 | 
					                      onTap: () => mediaController.hideVideoControls(),
 | 
				
			||||||
                      padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
					                      behavior: HitTestBehavior.translucent,
 | 
				
			||||||
                      child: Column(
 | 
					                      child: Visibility(
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
					                        visible: isReady,
 | 
				
			||||||
                        children: [
 | 
					                        child: SingleChildScrollView(
 | 
				
			||||||
                          Wrap(
 | 
					                          child: Padding(
 | 
				
			||||||
                            spacing: 6.0,
 | 
					                            padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                            runSpacing: 4.0,
 | 
					                            child: Column(
 | 
				
			||||||
                            alignment: WrapAlignment.center,
 | 
					                              crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
                            children: [
 | 
					                              children: [
 | 
				
			||||||
                              ...item.tags?.map(
 | 
					                                Wrap(
 | 
				
			||||||
                                    (tag) => ActionTag(
 | 
					                                  spacing: 6.0,
 | 
				
			||||||
                                      tag,
 | 
					                                  runSpacing: 4.0,
 | 
				
			||||||
                                      (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
					                                  alignment: WrapAlignment.center,
 | 
				
			||||||
                                          ? (onTagTap) => {}
 | 
					                                  children: [
 | 
				
			||||||
                                          : (onTagTap) {
 | 
					                                    ...tagsToShow.map(
 | 
				
			||||||
                                              mediaController.setTag(onTagTap);
 | 
					                                      (tag) => ActionTag(
 | 
				
			||||||
                                              Get.offAllNamed('/');
 | 
					                                        tag,
 | 
				
			||||||
                                            },
 | 
					                                        (tag.tag == 'sfw' || tag.tag == 'nsfw')
 | 
				
			||||||
 | 
					                                            ? (onTagTap) => {}
 | 
				
			||||||
 | 
					                                            : (onTagTap) {
 | 
				
			||||||
 | 
					                                                mediaController.setTag(
 | 
				
			||||||
 | 
					                                                  onTagTap,
 | 
				
			||||||
 | 
					                                                );
 | 
				
			||||||
 | 
					                                                Get.offAllNamed('/');
 | 
				
			||||||
 | 
					                                              },
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
                                  ) ??
 | 
					                                  ],
 | 
				
			||||||
                                  [],
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          Obx(
 | 
					 | 
				
			||||||
                            () => Visibility(
 | 
					 | 
				
			||||||
                              visible: authController.isLoggedIn,
 | 
					 | 
				
			||||||
                              child: Padding(
 | 
					 | 
				
			||||||
                                padding: const EdgeInsets.only(top: 20.0),
 | 
					 | 
				
			||||||
                                child: FavoriteSection(
 | 
					 | 
				
			||||||
                                  item: item,
 | 
					 | 
				
			||||||
                                  index: index,
 | 
					 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                                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,
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                  ),
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ],
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    )
 | 
					                    ),
 | 
				
			||||||
                  else
 | 
					                  ),
 | 
				
			||||||
                    const SizedBox.shrink(),
 | 
					                  const SafeArea(child: SizedBox.shrink()),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					              persistentFooterButtons: mediaController.tag.value != null
 | 
				
			||||||
 | 
					                  ? [TagFooter()]
 | 
				
			||||||
 | 
					                  : null,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            persistentFooterButtons: mediaController.tag.value != null
 | 
					 | 
				
			||||||
                ? [TagFooter()]
 | 
					 | 
				
			||||||
                : null,
 | 
					 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,11 +28,11 @@ class EndDrawer extends StatelessWidget {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Obx(() {
 | 
					          Obx(() {
 | 
				
			||||||
            if (authController.token.value != null &&
 | 
					            if (authController.token.value != null &&
 | 
				
			||||||
                authController.avatarUrl.value != null) {
 | 
					                authController.user.value?.avatarUrl != null) {
 | 
				
			||||||
              return DrawerHeader(
 | 
					              return DrawerHeader(
 | 
				
			||||||
                decoration: BoxDecoration(
 | 
					                decoration: BoxDecoration(
 | 
				
			||||||
                  image: DecorationImage(
 | 
					                  image: DecorationImage(
 | 
				
			||||||
                    image: NetworkImage(authController.avatarUrl.value!),
 | 
					                    image: NetworkImage(authController.user.value!.avatarUrl!),
 | 
				
			||||||
                    fit: BoxFit.cover,
 | 
					                    fit: BoxFit.cover,
 | 
				
			||||||
                    alignment: Alignment.topCenter,
 | 
					                    alignment: Alignment.topCenter,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
@@ -58,11 +58,11 @@ class EndDrawer extends StatelessWidget {
 | 
				
			|||||||
                padding: const EdgeInsets.all(16.0),
 | 
					                padding: const EdgeInsets.all(16.0),
 | 
				
			||||||
                child: Column(
 | 
					                child: Column(
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    if (authController.username.value != null)
 | 
					                    if (authController.user.value?.username != null)
 | 
				
			||||||
                      Padding(
 | 
					                      Padding(
 | 
				
			||||||
                        padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
					                        padding: const EdgeInsets.symmetric(vertical: 8.0),
 | 
				
			||||||
                        child: Text(
 | 
					                        child: Text(
 | 
				
			||||||
                          'Hamlo ${authController.username.value!}',
 | 
					                          'Hamlo ${authController.user.value?.username}',
 | 
				
			||||||
                          style: const TextStyle(fontWeight: FontWeight.bold),
 | 
					                          style: const TextStyle(fontWeight: FontWeight.bold),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ class FavoriteSection extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final bool isFavorite =
 | 
					    final bool isFavorite =
 | 
				
			||||||
        item.favorites?.any((f) => f.userId == authController.userId.value) ??
 | 
					        item.favorites?.any((f) => f.userId == authController.user.value?.id) ??
 | 
				
			||||||
        false;
 | 
					        false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Row(
 | 
					    return Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
					import 'package:cached_video_player_plus/cached_video_player_plus.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class VideoControlsOverlay extends StatelessWidget {
 | 
					class VideoControlsOverlay extends StatefulWidget {
 | 
				
			||||||
  final CachedVideoPlayerPlusController controller;
 | 
					  final CachedVideoPlayerPlusController controller;
 | 
				
			||||||
  final VoidCallback onOverlayTap;
 | 
					  final VoidCallback onOverlayTap;
 | 
				
			||||||
  final bool muted;
 | 
					  final bool muted;
 | 
				
			||||||
@@ -16,51 +18,102 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
    required this.onMuteToggle,
 | 
					    required this.onMuteToggle,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _VideoControlsOverlayState extends State<VideoControlsOverlay> {
 | 
				
			||||||
 | 
					  bool _showSeekIndicator = false;
 | 
				
			||||||
 | 
					  bool _isRewinding = false;
 | 
				
			||||||
 | 
					  Timer? _hideTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _hideTimer?.cancel();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _handleDoubleTap(TapDownDetails details) {
 | 
				
			||||||
 | 
					    final screenWidth = MediaQuery.of(context).size.width;
 | 
				
			||||||
 | 
					    final isRewind = details.globalPosition.dx < screenWidth / 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Future(() {
 | 
				
			||||||
 | 
					      if (isRewind) {
 | 
				
			||||||
 | 
					        final newPosition =
 | 
				
			||||||
 | 
					            widget.controller.value.position - const Duration(seconds: 10);
 | 
				
			||||||
 | 
					        widget.controller.seekTo(
 | 
				
			||||||
 | 
					          newPosition < Duration.zero ? Duration.zero : newPosition,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        final newPosition =
 | 
				
			||||||
 | 
					            widget.controller.value.position + const Duration(seconds: 10);
 | 
				
			||||||
 | 
					        final duration = widget.controller.value.duration;
 | 
				
			||||||
 | 
					        widget.controller.seekTo(
 | 
				
			||||||
 | 
					          newPosition > duration ? duration : newPosition,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _hideTimer?.cancel();
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _showSeekIndicator = true;
 | 
				
			||||||
 | 
					      _isRewinding = isRewind;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    _hideTimer = Timer(const Duration(milliseconds: 500), () {
 | 
				
			||||||
 | 
					      setState(() => _showSeekIndicator = false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      alignment: Alignment.center,
 | 
					      alignment: Alignment.center,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Row(
 | 
					        GestureDetector(
 | 
				
			||||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
					          onTap: widget.onOverlayTap,
 | 
				
			||||||
          children: [
 | 
					          onDoubleTapDown: _handleDoubleTap,
 | 
				
			||||||
            _ControlButton(Icons.replay_10, () {
 | 
					          child: Container(color: Colors.transparent),
 | 
				
			||||||
              onOverlayTap();
 | 
					        ),
 | 
				
			||||||
              Duration newPosition =
 | 
					        AnimatedOpacity(
 | 
				
			||||||
                  controller.value.position - const Duration(seconds: 10);
 | 
					          opacity: _showSeekIndicator ? 1.0 : 0.0,
 | 
				
			||||||
              if (newPosition < Duration.zero) newPosition = Duration.zero;
 | 
					          duration: const Duration(milliseconds: 200),
 | 
				
			||||||
              controller.seekTo(newPosition);
 | 
					          child: Align(
 | 
				
			||||||
            }),
 | 
					            alignment: _isRewinding
 | 
				
			||||||
            const SizedBox(width: 40),
 | 
					                ? Alignment.centerLeft
 | 
				
			||||||
            _ControlButton(
 | 
					                : Alignment.centerRight,
 | 
				
			||||||
              controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
					            child: Padding(
 | 
				
			||||||
              () {
 | 
					              padding: const EdgeInsets.symmetric(horizontal: 40.0),
 | 
				
			||||||
                onOverlayTap();
 | 
					              child: Icon(
 | 
				
			||||||
                controller.value.isPlaying
 | 
					                _isRewinding
 | 
				
			||||||
                    ? controller.pause()
 | 
					                    ? Icons.fast_rewind_rounded
 | 
				
			||||||
                    : controller.play();
 | 
					                    : Icons.fast_forward_rounded,
 | 
				
			||||||
              },
 | 
					                color: Colors.white70,
 | 
				
			||||||
              size: 64,
 | 
					                size: 60,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            const SizedBox(width: 40),
 | 
					          ),
 | 
				
			||||||
            _ControlButton(Icons.forward_10, () {
 | 
					        ),
 | 
				
			||||||
              onOverlayTap();
 | 
					        _ControlButton(
 | 
				
			||||||
              Duration newPosition =
 | 
					          widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
 | 
				
			||||||
                  controller.value.position + const Duration(seconds: 10);
 | 
					          () {
 | 
				
			||||||
              if (newPosition > controller.value.duration) {
 | 
					            widget.onOverlayTap();
 | 
				
			||||||
                newPosition = controller.value.duration;
 | 
					            widget.controller.value.isPlaying
 | 
				
			||||||
              }
 | 
					                ? widget.controller.pause()
 | 
				
			||||||
              controller.seekTo(newPosition);
 | 
					                : widget.controller.play();
 | 
				
			||||||
            }),
 | 
					          },
 | 
				
			||||||
          ],
 | 
					          size: 64,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        Positioned(
 | 
					        Positioned(
 | 
				
			||||||
          right: 12,
 | 
					          right: 12,
 | 
				
			||||||
          bottom: 12,
 | 
					          bottom: 12,
 | 
				
			||||||
          child: _ControlButton(muted ? Icons.volume_off : Icons.volume_up, () {
 | 
					          child: _ControlButton(
 | 
				
			||||||
            onOverlayTap();
 | 
					            widget.muted ? Icons.volume_off : Icons.volume_up,
 | 
				
			||||||
            onMuteToggle();
 | 
					            () {
 | 
				
			||||||
          }, size: 16),
 | 
					              widget.onOverlayTap();
 | 
				
			||||||
 | 
					              widget.onMuteToggle();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            size: 16,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        Align(
 | 
					        Align(
 | 
				
			||||||
          alignment: Alignment.bottomCenter,
 | 
					          alignment: Alignment.bottomCenter,
 | 
				
			||||||
@@ -73,16 +126,16 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
                  left: 10,
 | 
					                  left: 10,
 | 
				
			||||||
                  bottom: 12,
 | 
					                  bottom: 12,
 | 
				
			||||||
                  child: Text(
 | 
					                  child: Text(
 | 
				
			||||||
                    '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
 | 
					                    '${_formatDuration(widget.controller.value.position)} / ${_formatDuration(widget.controller.value.duration)}',
 | 
				
			||||||
                    style: const TextStyle(color: Colors.white, fontSize: 12),
 | 
					                    style: const TextStyle(color: Colors.white, fontSize: 12),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                Listener(
 | 
					                Listener(
 | 
				
			||||||
                  onPointerDown: (_) {
 | 
					                  onPointerDown: (_) {
 | 
				
			||||||
                    onOverlayTap();
 | 
					                    widget.onOverlayTap();
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                  child: VideoProgressIndicator(
 | 
					                  child: VideoProgressIndicator(
 | 
				
			||||||
                    controller,
 | 
					                    widget.controller,
 | 
				
			||||||
                    allowScrubbing: true,
 | 
					                    allowScrubbing: true,
 | 
				
			||||||
                    padding: const EdgeInsets.only(top: 25.0),
 | 
					                    padding: const EdgeInsets.only(top: 25.0),
 | 
				
			||||||
                    colors: const VideoProgressColors(
 | 
					                    colors: const VideoProgressColors(
 | 
				
			||||||
@@ -92,11 +145,15 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                if (controller.value.duration.inMilliseconds > 0)
 | 
					                if (widget.controller.value.duration.inMilliseconds > 0)
 | 
				
			||||||
                  Positioned(
 | 
					                  Positioned(
 | 
				
			||||||
                    left:
 | 
					                    left:
 | 
				
			||||||
                        (controller.value.position.inMilliseconds /
 | 
					                        (widget.controller.value.position.inMilliseconds /
 | 
				
			||||||
                                controller.value.duration.inMilliseconds) *
 | 
					                                widget
 | 
				
			||||||
 | 
					                                    .controller
 | 
				
			||||||
 | 
					                                    .value
 | 
				
			||||||
 | 
					                                    .duration
 | 
				
			||||||
 | 
					                                    .inMilliseconds) *
 | 
				
			||||||
                            MediaQuery.of(context).size.width -
 | 
					                            MediaQuery.of(context).size.width -
 | 
				
			||||||
                        6,
 | 
					                        6,
 | 
				
			||||||
                    bottom: -4,
 | 
					                    bottom: -4,
 | 
				
			||||||
@@ -118,7 +175,8 @@ class VideoControlsOverlay extends StatelessWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String _formatDuration(Duration duration) {
 | 
					  String _formatDuration(Duration? duration) {
 | 
				
			||||||
 | 
					    if (duration == null) return '00:00';
 | 
				
			||||||
    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
					    String twoDigits(int n) => n.toString().padLeft(2, '0');
 | 
				
			||||||
    return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
 | 
					    return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,8 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
  final MediaController controller = Get.find<MediaController>();
 | 
					  final MediaController controller = Get.find<MediaController>();
 | 
				
			||||||
  late CachedVideoPlayerPlusController _controller;
 | 
					  late CachedVideoPlayerPlusController _controller;
 | 
				
			||||||
  late Worker _muteWorker;
 | 
					  late Worker _muteWorker;
 | 
				
			||||||
 | 
					  late Worker _timerResetWorker;
 | 
				
			||||||
 | 
					  late Worker _hideControlsWorker;
 | 
				
			||||||
  bool _showControls = false;
 | 
					  bool _showControls = false;
 | 
				
			||||||
  Timer? _hideControlsTimer;
 | 
					  Timer? _hideControlsTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,11 +46,26 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
        _controller.setVolume(muted ? 0.0 : 1.0);
 | 
					        _controller.setVolume(muted ? 0.0 : 1.0);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    _timerResetWorker = ever(controller.videoControlsTimerNotifier, (_) {
 | 
				
			||||||
 | 
					      if (widget.isActive && mounted) {
 | 
				
			||||||
 | 
					        if (!_showControls) {
 | 
				
			||||||
 | 
					          setState(() => _showControls = true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        _startHideControlsTimer();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    _hideControlsWorker = ever(controller.hideControlsNotifier, (_) {
 | 
				
			||||||
 | 
					      if (mounted && _showControls) {
 | 
				
			||||||
 | 
					        setState(() => _showControls = false);
 | 
				
			||||||
 | 
					        _hideControlsTimer?.cancel();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _initController() async {
 | 
					  Future<void> _initController() async {
 | 
				
			||||||
    _controller = CachedVideoPlayerPlusController.networkUrl(
 | 
					    _controller = CachedVideoPlayerPlusController.networkUrl(
 | 
				
			||||||
      Uri.parse(widget.details.mediaUrl),
 | 
					      Uri.parse(widget.details.mediaUrl),
 | 
				
			||||||
 | 
					      videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    await _controller.initialize();
 | 
					    await _controller.initialize();
 | 
				
			||||||
    widget.onInitialized?.call();
 | 
					    widget.onInitialized?.call();
 | 
				
			||||||
@@ -78,20 +95,35 @@ class _VideoWidgetState extends State<VideoWidget> {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _muteWorker.dispose();
 | 
					    _muteWorker.dispose();
 | 
				
			||||||
 | 
					    _timerResetWorker.dispose();
 | 
				
			||||||
 | 
					    _hideControlsWorker.dispose();
 | 
				
			||||||
    _controller.dispose();
 | 
					    _controller.dispose();
 | 
				
			||||||
    _hideControlsTimer?.cancel();
 | 
					    _hideControlsTimer?.cancel();
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onTap({bool ctrlButton = false}) {
 | 
					  void _startHideControlsTimer() {
 | 
				
			||||||
    if (!ctrlButton) {
 | 
					    _hideControlsTimer?.cancel();
 | 
				
			||||||
      setState(() => _showControls = !_showControls);
 | 
					    _hideControlsTimer = Timer(const Duration(seconds: 3), () {
 | 
				
			||||||
    }
 | 
					      if (mounted) {
 | 
				
			||||||
    if (_showControls) {
 | 
					 | 
				
			||||||
      _hideControlsTimer?.cancel();
 | 
					 | 
				
			||||||
      _hideControlsTimer = Timer(const Duration(seconds: 2), () {
 | 
					 | 
				
			||||||
        setState(() => _showControls = false);
 | 
					        setState(() => _showControls = false);
 | 
				
			||||||
      });
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _onTap({bool ctrlButton = false}) {
 | 
				
			||||||
 | 
					    if (ctrlButton) {
 | 
				
			||||||
 | 
					      _startHideControlsTimer();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final bool newShowState = !_showControls;
 | 
				
			||||||
 | 
					    setState(() => _showControls = newShowState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (newShowState) {
 | 
				
			||||||
 | 
					      _startHideControlsTimer();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _hideControlsTimer?.cancel();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 1.4.1+62
 | 
					version: 1.4.2+63
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.9.0-100.2.beta
 | 
					  sdk: ^3.9.0-100.2.beta
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user