aktueller Stand

This commit is contained in:
2025-05-31 19:44:32 +02:00
parent 018581e4d2
commit 30f8f3133a
13 changed files with 375 additions and 140 deletions

View File

@ -2,17 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:f0ckapp/mediagrid.dart';
//import 'package:media_kit/media_kit.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
//MediaKit.ensureInitialized();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp
]).then((_) {
runApp(const F0ckApp());
});
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp(const F0ckApp());
}
class F0ckApp extends StatelessWidget {
@ -25,13 +19,6 @@ class F0ckApp extends StatelessWidget {
scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23),
),
home: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
title: const Text('f0cks'),
centerTitle: true,
),
body: MediaGrid(),
),
);

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:f0ckapp/services/api.dart';
import 'package:f0ckapp/models/mediaitem.dart';
import 'package:f0ckapp/screens/detail.dart';
import 'dart:async';
class MediaGrid extends StatefulWidget {
const MediaGrid({super.key});
@ -15,64 +15,168 @@ class _MediaGridState extends State<MediaGrid> {
final ScrollController _scrollController = ScrollController();
List<MediaItem> mediaItems = [];
bool isLoading = false;
Timer? _debounceTimer;
Completer<void>? _navigationCompleter;
int _crossAxisCount = 3;
@override
void initState() {
super.initState();
_loadMedia();
_scrollController.addListener(() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
_loadMedia();
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
_debounceLoadMedia();
}
});
}
void _debounceLoadMedia() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), _loadMedia);
}
Future<void> _loadMedia() async {
if (isLoading) return;
setState(() => isLoading = true);
try {
List<MediaItem> newMedia = await fetchMedia(older: mediaItems.isNotEmpty ? mediaItems.last.id.toString() : null);
setState(() {
mediaItems.addAll(newMedia);
isLoading = false;
});
final newMedia = await fetchMedia(
older: mediaItems.isNotEmpty ? mediaItems.last.id.toString() : null,
);
if (mounted) {
setState(() => mediaItems.addAll(newMedia));
}
} catch (e) {
print('Fehler: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der Medien: $e')),
);
}
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Future<void> _refreshMedia() async {
setState(() => isLoading = true);
try {
final freshMedia = await fetchMedia(older: null);
if (mounted) {
setState(() {
mediaItems.clear();
mediaItems.addAll(freshMedia);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Aktualisieren: $e')),
);
}
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Future<void> _navigateToDetail(MediaItem item) async {
print('Tapped item: ${item.id}');
if (_navigationCompleter?.isCompleted == false) return;
_navigationCompleter = Completer();
try {
final details = await fetchMediaDetail(item.id);
if (mounted) {
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailView(details: details)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der Details: $e')),
);
}
} finally {
_navigationCompleter?.complete();
}
}
@override
Widget build(BuildContext context) {
return GridView.builder(
controller: _scrollController,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8.0,
mainAxisSpacing: 8.0,
return Scaffold(
appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('f0cks'),
DropdownButton<int>(
value: _crossAxisCount,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: const Color.fromARGB(255, 255, 255, 255),
items: [3, 4].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text('$value Spalten', style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_crossAxisCount = newValue;
});
}
},
),
],
),
),
itemCount: mediaItems.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaItems[index];
return GestureDetector(
onTap: () async {
try {
final details = await fetchMediaDetail(item.id);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailView(details: details)),
);
} catch (e) {
print('Fehler beim Laden der Details: $e');
body: RefreshIndicator(
onRefresh: _refreshMedia,
child: GridView.builder(
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _crossAxisCount,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount: mediaItems.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaItems[index];
final mode =
{1: Colors.green, 2: Colors.red}[item.tagid] ?? Colors.yellow;
return InkWell(
onTap: () => _navigateToDetail(item),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Image.network(item.thumbnailUrl, fit: BoxFit.cover),
Align(
alignment: FractionalOffset.bottomRight,
child: Icon(Icons.square, color: mode, size: 15.0),
),
],
),
);
},
child: Image.network(item.thumbnailUrl, fit: BoxFit.cover),
);
},
),
),
);
}
@override
void dispose() {
_scrollController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
}

View File

@ -4,8 +4,8 @@ class MediaItemDetail {
final String dest;
final String username;
final int stamp;
final int next;
final int prev;
final int? next;
final int? prev;
MediaItemDetail({
required this.id,

View File

@ -1,82 +0,0 @@
import 'package:f0ckapp/services/api.dart';
import 'package:flutter/material.dart';
import 'package:f0ckapp/models/mediaitem_detail.dart';
import 'package:f0ckapp/widgets/video_widget.dart';
class DetailScreen extends StatelessWidget {
final MediaItemDetail details;
const DetailScreen({super.key, required this.details});
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dx > 0) {
_navigateToPrev(context);
} else {
_navigateToNext(context);
}
},
child: VideoWidget(details: this.details)
);
//return VideoWidget(url: this.details.mediaUrl);
/*return GestureDetector(
onHorizontalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dx > 0) {
_navigateToPrev(context);
} else {
_navigateToNext(context);
}
},
child: Scaffold(
body: Column(
children: [
if (this.details.mime.startsWith('image'))
ClipRRect(
child: Image.network(this.details.mediaUrl, fit: BoxFit.cover),
)
else
VideoPost(
url: this.details.mediaUrl,
/*appBar: AppBar(
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
title: const Text('f0ck'),
centerTitle: true,
)*/
),
//const SizedBox(height: 10),
//Text('f0cked by: ${this.details.username}', style: const TextStyle(fontSize: 16, color: Color.fromARGB(255, 255, 255, 255))),
],
),
),
);*/
}
void _navigateToNext(BuildContext context) async {
try {
final nextDetails = await fetchMediaDetail(details.next);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DetailScreen(details: nextDetails)),
);
} catch (e) {
print('Fehler beim Laden des nächsten Items: $e');
}
}
void _navigateToPrev(BuildContext context) async {
try {
final prevDetails = await fetchMediaDetail(details.prev);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DetailScreen(details: prevDetails)),
);
} catch (e) {
print('Fehler beim Laden des vorherigen Items: $e');
}
}
}

View File

@ -84,10 +84,10 @@ class _DetailViewState extends State<DetailView> {
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragEnd: (details) {
if (details.velocity.pixelsPerSecond.dx > 0) {
_loadNewMediaItem(widget.details.next, true);
} else if (details.velocity.pixelsPerSecond.dx < 0) {
_loadNewMediaItem(widget.details.prev, false);
if (details.velocity.pixelsPerSecond.dx > 0 && widget.details.next != null) {
_loadNewMediaItem(widget.details.next!, true);
} else if (details.velocity.pixelsPerSecond.dx < 0 && widget.details.prev != null) {
_loadNewMediaItem(widget.details.prev!, false);
}
},
child: Scaffold(

View File

@ -21,7 +21,7 @@ Future<List<MediaItem>> fetchMedia({String? older}) async {
}
Future<MediaItemDetail> fetchMediaDetail(int itemId) async {
final Uri url = Uri.parse('https://f0ck.me/api/v2/item/$itemId');
final Uri url = Uri.parse('https://f0ck.me/api/v2/item/${itemId.toString()}');
final response = await http.get(url);
if (response.statusCode == 200) {