1.0.17
- new appicon - smartRefreshIndicator (https://github.com/flutter/flutter/issues/65356#issuecomment-2410727567)
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 680 KiB After Width: | Height: | Size: 447 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 906 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
@ -15,6 +15,7 @@ class F0ckApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23),
|
||||
),
|
||||
|
@ -13,7 +13,7 @@ class MediaGrid extends StatefulWidget {
|
||||
|
||||
class _MediaGridState extends State<MediaGrid> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final String _version = '1.0.15';
|
||||
final String _version = '1.0.17';
|
||||
List<MediaItem> mediaItems = [];
|
||||
bool isLoading = false;
|
||||
Timer? _debounceTimer;
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:f0ckapp/models/mediaitem.dart';
|
||||
import 'package:f0ckapp/services/api.dart';
|
||||
import 'package:f0ckapp/widgets/video_widget.dart';
|
||||
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
|
||||
|
||||
class DetailView extends StatefulWidget {
|
||||
final int initialItemId;
|
||||
@ -26,33 +27,32 @@ class DetailView extends StatefulWidget {
|
||||
class _DetailViewState extends State<DetailView> {
|
||||
late PageController _pageController;
|
||||
late List<MediaItem> mediaItems;
|
||||
final List<String> _modes = ["sfw", "nsfw", "untagged", "all"];
|
||||
int currentItemId = 0;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
mediaItems = widget.mediaItems;
|
||||
final initialIndex = mediaItems.indexWhere(
|
||||
(item) => item.id == widget.initialItemId,
|
||||
);
|
||||
_pageController = PageController(initialPage: initialIndex);
|
||||
|
||||
currentItemId = mediaItems[initialIndex].id;
|
||||
|
||||
_pageController.addListener(() {
|
||||
final newIndex = _pageController.page?.round();
|
||||
if (newIndex != null && newIndex < mediaItems.length) {
|
||||
setState(() => currentItemId = mediaItems[newIndex].id);
|
||||
}
|
||||
_pageController.addListener(_onPageScroll);
|
||||
}
|
||||
|
||||
if (_pageController.position.pixels >=
|
||||
_pageController.position.maxScrollExtent - 100) {
|
||||
_loadMoreMedia();
|
||||
}
|
||||
});
|
||||
void _onPageScroll() {
|
||||
final newIndex = _pageController.page?.round();
|
||||
if (newIndex != null && newIndex < mediaItems.length) {
|
||||
setState(() => currentItemId = mediaItems[newIndex].id);
|
||||
}
|
||||
|
||||
if (_pageController.position.pixels >=
|
||||
_pageController.position.maxScrollExtent - 100) {
|
||||
_loadMoreMedia();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMoreMedia() async {
|
||||
@ -66,15 +66,11 @@ class _DetailViewState extends State<DetailView> {
|
||||
mode: widget.mode,
|
||||
random: widget.random,
|
||||
);
|
||||
if (mounted) {
|
||||
if (mounted && newMedia.isNotEmpty) {
|
||||
setState(() => mediaItems.addAll(newMedia));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Fehler beim Laden weiterer Medien: $e')),
|
||||
);
|
||||
}
|
||||
_showError("Fehler beim Laden weiterer Medien: $e");
|
||||
} finally {
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
@ -84,21 +80,21 @@ class _DetailViewState extends State<DetailView> {
|
||||
try {
|
||||
final updatedItem = await fetchMediaDetail(currentItemId);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
final index = mediaItems.indexWhere(
|
||||
(item) => item.id == currentItemId,
|
||||
);
|
||||
if (index != -1) {
|
||||
mediaItems[index] = updatedItem;
|
||||
}
|
||||
});
|
||||
final index = mediaItems.indexWhere((item) => item.id == currentItemId);
|
||||
if (index != -1) {
|
||||
setState(() => mediaItems[index] = updatedItem);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Fehler beim Aktualisieren des Items: $e')),
|
||||
);
|
||||
}
|
||||
_showError("Fehler beim Aktualisieren des Items: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _showError(String message) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,70 +105,60 @@ class _DetailViewState extends State<DetailView> {
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF2B2B2B),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text(
|
||||
'f0ck #$currentItemId (${widget.type}, ${_modes[widget.mode]})',
|
||||
),
|
||||
title: Text('f0ck #$currentItemId (${widget.type})'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refreshMediaItem,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: mediaItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = mediaItems[index];
|
||||
return Column(
|
||||
children: [
|
||||
if (item.mime.startsWith('image'))
|
||||
Image.network(item.mediaUrl, fit: BoxFit.contain)
|
||||
else
|
||||
VideoWidget(details: item),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
item.mime,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5.0,
|
||||
children: item.tags.map((tag) {
|
||||
Color tagColor;
|
||||
switch (tag.id) {
|
||||
case 1:
|
||||
tagColor = Colors.green;
|
||||
break;
|
||||
case 2:
|
||||
tagColor = Colors.red;
|
||||
break;
|
||||
default:
|
||||
tagColor = const Color(0xFF090909);
|
||||
}
|
||||
|
||||
return Chip(
|
||||
label: Text(tag.tag),
|
||||
backgroundColor: tagColor,
|
||||
labelStyle: const TextStyle(color: Colors.white),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: mediaItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final MediaItem item = mediaItems[index];
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: SmartRefreshIndicator(
|
||||
onRefresh: _refreshMediaItem,
|
||||
child: _buildMediaItem(item)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaItem(MediaItem item) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (item.mime.startsWith('image'))
|
||||
Image.network(
|
||||
item.mediaUrl,
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
else
|
||||
VideoWidget(details: item),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
item.mime,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 18),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 5.0,
|
||||
children: item.tags.map((tag) {
|
||||
return Chip(
|
||||
label: Text(tag.tag),
|
||||
backgroundColor: {
|
||||
1: Colors.green,
|
||||
2: Colors.red
|
||||
}[tag.id] ?? const Color(0xFF090909),
|
||||
labelStyle: const TextStyle(color: Colors.white),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
28
lib/utils/SmartRefreshIndicator.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SmartRefreshIndicator extends StatelessWidget {
|
||||
final Future<void> Function() onRefresh;
|
||||
final Widget child;
|
||||
|
||||
const SmartRefreshIndicator({
|
||||
super.key,
|
||||
required this.onRefresh,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => RefreshIndicator(
|
||||
onRefresh: onRefresh,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -52,7 +52,9 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: isAudio ? 1.0 : _controller.value.aspectRatio,
|
||||
aspectRatio: _controller.value.isInitialized
|
||||
? _controller.value.aspectRatio
|
||||
: 9 / 16,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
@ -66,104 +68,108 @@ class _VideoWidgetState extends State<VideoWidget> {
|
||||
},
|
||||
child: isAudio
|
||||
? Image.network(widget.details.coverUrl, fit: BoxFit.cover)
|
||||
: VideoPlayer(_controller),
|
||||
: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Stack(
|
||||
if (_controller.value.isInitialized)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 10,
|
||||
child: VideoProgressIndicator(
|
||||
_controller,
|
||||
allowScrubbing: true,
|
||||
padding: EdgeInsets.only(bottom: 0),
|
||||
colors: VideoProgressColors(
|
||||
playedColor: Colors.red,
|
||||
bufferedColor: Colors.grey,
|
||||
backgroundColor: Colors.black.withAlpha(128),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left:
|
||||
(_controller.value.position.inMilliseconds /
|
||||
_controller.value.duration.inMilliseconds) *
|
||||
MediaQuery.of(context).size.width -
|
||||
6,
|
||||
bottom: -1,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_controller.value.isInitialized)
|
||||
SizedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(_controller.value.position),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 10,
|
||||
child: VideoProgressIndicator(
|
||||
_controller,
|
||||
allowScrubbing: true,
|
||||
padding: EdgeInsets.only(bottom: 0),
|
||||
colors: VideoProgressColors(
|
||||
playedColor: Colors.red,
|
||||
bufferedColor: Colors.grey,
|
||||
backgroundColor: Colors.black.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.replay_10),
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
_controller.seekTo(
|
||||
_controller.value.position -
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left:
|
||||
(_controller.value.position.inMilliseconds /
|
||||
_controller.value.duration.inMilliseconds) *
|
||||
MediaQuery.of(context).size.width -
|
||||
6,
|
||||
bottom: -1,
|
||||
child: Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white,
|
||||
border: Border.all(color: Colors.red, width: 2),
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(
|
||||
_controller.value.isPlaying
|
||||
? Icons.pause
|
||||
: Icons.play_arrow,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying
|
||||
? _controller.pause()
|
||||
: _controller.play();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.forward_10),
|
||||
onPressed: () {
|
||||
_controller.seekTo(
|
||||
_controller.value.position +
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
_formatDuration(_controller.value.duration),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_formatDuration(_controller.value.position),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.replay_10),
|
||||
color: Colors.white,
|
||||
onPressed: () {
|
||||
_controller.seekTo(
|
||||
_controller.value.position -
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(
|
||||
_controller.value.isPlaying
|
||||
? Icons.pause
|
||||
: Icons.play_arrow,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying
|
||||
? _controller.pause()
|
||||
: _controller.play();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.forward_10),
|
||||
onPressed: () {
|
||||
_controller.seekTo(
|
||||
_controller.value.position +
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
_formatDuration(_controller.value.duration),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -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.0.15
|
||||
version: 1.0.17
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0-172.0.dev
|
||||
|