v1.4.2+63
All checks were successful
Flutter Schmutter / build (push) Successful in 3m51s

This commit is contained in:
2025-06-21 16:28:57 +02:00
parent 73a44bb269
commit 7a88c23e57
10 changed files with 285 additions and 156 deletions

View File

@ -4,14 +4,14 @@ import 'package:get/get.dart';
import 'package:encrypt_shared_preferences/provider.dart';
import 'package:http/http.dart' as http;
import 'package:f0ckapp/models/user.dart';
class AuthController extends GetxController {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
RxnString token = RxnString();
RxnInt userId = RxnInt();
RxnString avatarUrl = RxnString();
RxnString username = RxnString();
Rxn<User> user = Rxn<User>();
RxBool isLoading = false.obs;
RxnString error = RxnString();
@ -47,9 +47,7 @@ class AuthController extends GetxController {
} catch (_) {}
}
token.value = null;
userId.value = null;
avatarUrl.value = null;
username.value = null;
user.value = null;
await storage.remove('token');
}
@ -66,11 +64,7 @@ class AuthController extends GetxController {
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'];
user.value = User.fromJson(data);
return true;
} else {
error.value = 'Kein Token erhalten';
@ -95,13 +89,7 @@ class AuthController extends GetxController {
);
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'];
user.value = User.fromJson(data);
} else {
await logout();
}

View File

@ -27,6 +27,8 @@ class MediaController extends GetxController {
Rx<PageTransition> 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;
@ -163,6 +165,9 @@ class MediaController extends GetxController {
fetchInitial();
}
void resetVideoControlsTimer() => videoControlsTimerNotifier.value++;
void hideVideoControls() => hideControlsNotifier.value++;
@override
void onInit() async {
super.onInit();

View File

@ -186,7 +186,6 @@ final ThemeData f0ck95Theme = ThemeData(
backgroundColor: const Color(0xFFE0E0E0),
foregroundColor: Colors.black,
elevation: 4,
centerTitle: true,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black),

17
lib/models/user.dart Normal file
View 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,
);
}
}

View File

@ -32,8 +32,9 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
PageController? _pageController;
final MediaController mediaController = Get.find<MediaController>();
final AuthController authController = Get.find<AuthController>();
final _currentIndex = 0.obs;
final _mediaSaverChannel = const MethodChannel('MediaShit');
final RxInt _currentIndex = 0.obs;
final MethodChannel _mediaSaverChannel = const MethodChannel('MediaShit');
final Map<int, bool> _expandedTags = {};
bool _isLoading = true;
bool _itemNotFound = false;
@ -105,7 +106,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
void _onPageChanged(int idx) {
if (idx != _currentIndex.value) {
_currentIndex.value = idx;
final item = mediaController.items[idx];
final MediaItem item = mediaController.items[idx];
if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
setState(() => _readyItemIds.add(item.id));
}
@ -224,8 +225,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
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<Tag> allTags = item.tags ?? [];
final bool hasMoreTags = allTags.length > 5;
final List<Tag> tagsToShow = areTagsExpanded
? allTags
: allTags.take(5).toList();
return Scaffold(
return Obx(
() => Scaffold(
endDrawer: EndDrawer(),
endDrawerEnableOpenDragGesture:
mediaController.drawerSwipeEnabled.value,
@ -262,8 +270,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
),
],
),
body: SingleChildScrollView(
child: Column(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AnimatedBuilder(
@ -281,10 +288,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
() => _buildMedia(item, index == _currentIndex.value),
),
),
const SizedBox(height: 16),
if (isReady)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
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: [
@ -293,20 +305,35 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
runSpacing: 4.0,
alignment: WrapAlignment.center,
children: [
...item.tags?.map(
...tagsToShow.map(
(tag) => ActionTag(
tag,
(tag.tag == 'sfw' || tag.tag == 'nsfw')
? (onTagTap) => {}
: (onTagTap) {
mediaController.setTag(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,
@ -321,15 +348,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
),
],
),
)
else
const SizedBox.shrink(),
],
),
),
),
),
),
const SafeArea(child: SizedBox.shrink()),
],
),
persistentFooterButtons: mediaController.tag.value != null
? [TagFooter()]
: null,
),
);
},
),

View File

@ -28,11 +28,11 @@ class EndDrawer extends StatelessWidget {
children: [
Obx(() {
if (authController.token.value != null &&
authController.avatarUrl.value != null) {
authController.user.value?.avatarUrl != null) {
return DrawerHeader(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(authController.avatarUrl.value!),
image: NetworkImage(authController.user.value!.avatarUrl!),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
@ -58,11 +58,11 @@ class EndDrawer extends StatelessWidget {
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (authController.username.value != null)
if (authController.user.value?.username != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Hamlo ${authController.username.value!}',
'Hamlo ${authController.user.value?.username}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),

View File

@ -18,7 +18,7 @@ class FavoriteSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool isFavorite =
item.favorites?.any((f) => f.userId == authController.userId.value) ??
item.favorites?.any((f) => f.userId == authController.user.value?.id) ??
false;
return Row(

View File

@ -1,8 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
class VideoControlsOverlay extends StatelessWidget {
class VideoControlsOverlay extends StatefulWidget {
final CachedVideoPlayerPlusController controller;
final VoidCallback onOverlayTap;
final bool muted;
@ -16,51 +18,102 @@ class VideoControlsOverlay extends StatelessWidget {
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
Widget build(BuildContext context) {
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),
GestureDetector(
onTap: widget.onOverlayTap,
onDoubleTapDown: _handleDoubleTap,
child: Container(color: Colors.transparent),
),
AnimatedOpacity(
opacity: _showSeekIndicator ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200),
child: Align(
alignment: _isRewinding
? Alignment.centerLeft
: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Icon(
_isRewinding
? Icons.fast_rewind_rounded
: Icons.fast_forward_rounded,
color: Colors.white70,
size: 60,
),
),
),
),
_ControlButton(
controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
widget.controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
() {
onOverlayTap();
controller.value.isPlaying
? controller.pause()
: controller.play();
widget.onOverlayTap();
widget.controller.value.isPlaying
? widget.controller.pause()
: widget.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: _ControlButton(muted ? Icons.volume_off : Icons.volume_up, () {
onOverlayTap();
onMuteToggle();
}, size: 16),
child: _ControlButton(
widget.muted ? Icons.volume_off : Icons.volume_up,
() {
widget.onOverlayTap();
widget.onMuteToggle();
},
size: 16,
),
),
Align(
alignment: Alignment.bottomCenter,
@ -73,16 +126,16 @@ class VideoControlsOverlay extends StatelessWidget {
left: 10,
bottom: 12,
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),
),
),
Listener(
onPointerDown: (_) {
onOverlayTap();
widget.onOverlayTap();
},
child: VideoProgressIndicator(
controller,
widget.controller,
allowScrubbing: true,
padding: const EdgeInsets.only(top: 25.0),
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(
left:
(controller.value.position.inMilliseconds /
controller.value.duration.inMilliseconds) *
(widget.controller.value.position.inMilliseconds /
widget
.controller
.value
.duration
.inMilliseconds) *
MediaQuery.of(context).size.width -
6,
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');
return "${twoDigits(duration.inMinutes % 60)}:${twoDigits(duration.inSeconds % 60)}";
}

View File

@ -32,6 +32,8 @@ class _VideoWidgetState extends State<VideoWidget> {
final MediaController controller = Get.find<MediaController>();
late CachedVideoPlayerPlusController _controller;
late Worker _muteWorker;
late Worker _timerResetWorker;
late Worker _hideControlsWorker;
bool _showControls = false;
Timer? _hideControlsTimer;
@ -44,11 +46,26 @@ class _VideoWidgetState extends State<VideoWidget> {
_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 {
_controller = CachedVideoPlayerPlusController.networkUrl(
Uri.parse(widget.details.mediaUrl),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
await _controller.initialize();
widget.onInitialized?.call();
@ -78,21 +95,36 @@ class _VideoWidgetState extends State<VideoWidget> {
@override
void dispose() {
_muteWorker.dispose();
_timerResetWorker.dispose();
_hideControlsWorker.dispose();
_controller.dispose();
_hideControlsTimer?.cancel();
super.dispose();
}
void _onTap({bool ctrlButton = false}) {
if (!ctrlButton) {
setState(() => _showControls = !_showControls);
}
if (_showControls) {
void _startHideControlsTimer() {
_hideControlsTimer?.cancel();
_hideControlsTimer = Timer(const Duration(seconds: 2), () {
_hideControlsTimer = Timer(const Duration(seconds: 3), () {
if (mounted) {
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();
}
}
@override

View File

@ -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.1+62
version: 1.4.2+63
environment:
sdk: ^3.9.0-100.2.beta