first commit
This commit is contained in:
39
lib/main.dart
Normal file
39
lib/main.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:f0ckapp/mediagrid.dart';
|
||||
//import 'package:media_kit/media_kit.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
//MediaKit.ensureInitialized();
|
||||
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp
|
||||
]).then((_) {
|
||||
runApp(const F0ckApp());
|
||||
});
|
||||
}
|
||||
|
||||
class F0ckApp extends StatelessWidget {
|
||||
const F0ckApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
78
lib/mediagrid.dart
Normal file
78
lib/mediagrid.dart
Normal file
@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:f0ckapp/services/api.dart';
|
||||
import 'package:f0ckapp/models/mediaitem.dart';
|
||||
import 'package:f0ckapp/screens/detail.dart';
|
||||
|
||||
class MediaGrid extends StatefulWidget {
|
||||
const MediaGrid({super.key});
|
||||
|
||||
@override
|
||||
State createState() => _MediaGridState();
|
||||
}
|
||||
|
||||
class _MediaGridState extends State<MediaGrid> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
List<MediaItem> mediaItems = [];
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMedia();
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 100) {
|
||||
_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;
|
||||
});
|
||||
} catch (e) {
|
||||
print('Fehler: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
controller: _scrollController,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 8.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
),
|
||||
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');
|
||||
}
|
||||
},
|
||||
child: Image.network(item.thumbnailUrl, fit: BoxFit.cover),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
17
lib/models/mediaitem.dart
Normal file
17
lib/models/mediaitem.dart
Normal file
@ -0,0 +1,17 @@
|
||||
class MediaItem {
|
||||
final int id;
|
||||
final String mime;
|
||||
final int tagid;
|
||||
|
||||
MediaItem({required this.id, required this.mime, required this.tagid});
|
||||
|
||||
factory MediaItem.fromJson(Map<String, dynamic> json) {
|
||||
return MediaItem(
|
||||
id: json['id'],
|
||||
mime: json['mime'],
|
||||
tagid: json['tag_id'],
|
||||
);
|
||||
}
|
||||
|
||||
String get thumbnailUrl => 'https://f0ck.me/t/$id.webp';
|
||||
}
|
33
lib/models/mediaitem_detail.dart
Normal file
33
lib/models/mediaitem_detail.dart
Normal file
@ -0,0 +1,33 @@
|
||||
class MediaItemDetail {
|
||||
final int id;
|
||||
final String mime;
|
||||
final String dest;
|
||||
final String username;
|
||||
final int stamp;
|
||||
final int next;
|
||||
final int prev;
|
||||
|
||||
MediaItemDetail({
|
||||
required this.id,
|
||||
required this.mime,
|
||||
required this.dest,
|
||||
required this.username,
|
||||
required this.stamp,
|
||||
required this.next,
|
||||
required this.prev,
|
||||
});
|
||||
|
||||
factory MediaItemDetail.fromJson(Map<String, dynamic> json) {
|
||||
return MediaItemDetail(
|
||||
id: json['id'],
|
||||
mime: json['mime'],
|
||||
dest: json['dest'],
|
||||
username: json['username'],
|
||||
stamp: json['stamp'],
|
||||
next: json['next'],
|
||||
prev: json['prev'],
|
||||
);
|
||||
}
|
||||
|
||||
String get mediaUrl => 'https://f0ck.me/b/$dest';
|
||||
}
|
82
lib/screens/detail copy.dart
Normal file
82
lib/screens/detail copy.dart
Normal file
@ -0,0 +1,82 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
164
lib/screens/detail.dart
Normal file
164
lib/screens/detail.dart
Normal file
@ -0,0 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:f0ckapp/models/mediaitem_detail.dart';
|
||||
import 'package:f0ckapp/services/api.dart'; // API Call für neue Items
|
||||
|
||||
class DetailView extends StatefulWidget {
|
||||
final MediaItemDetail details;
|
||||
|
||||
const DetailView({super.key, required this.details});
|
||||
|
||||
@override
|
||||
State<DetailView> createState() => _DetailViewState();
|
||||
}
|
||||
|
||||
class _DetailViewState extends State<DetailView> {
|
||||
late VideoPlayerController _controller;
|
||||
late Future<void> _initializeVideoPlayer;
|
||||
bool isAudio = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
isAudio = widget.details.mime.startsWith("audio/");
|
||||
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.details.mediaUrl));
|
||||
|
||||
if (!widget.details.mime.startsWith("image/")) {
|
||||
_initializeVideoPlayer = _controller.initialize().then((_) {
|
||||
setState(() {
|
||||
_controller.play();
|
||||
_controller.setLooping(true);
|
||||
});
|
||||
}).catchError((error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Fehler beim Laden des Videos: $error")),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadNewMediaItem(int itemId, bool swipeLeft) async {
|
||||
try {
|
||||
var newDetails = await fetchMediaDetail(itemId); // API Call
|
||||
|
||||
Navigator.of(context).pushReplacement(PageRouteBuilder(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => DetailView(details: newDetails),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
Offset begin = swipeLeft ? const Offset(-1.0, 0.0) : const Offset(1.0, 0.0); // Richtung des Swipes
|
||||
const Offset end = Offset.zero;
|
||||
const curve = Curves.easeInOut;
|
||||
|
||||
var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
var offsetAnimation = animation.drive(tween);
|
||||
|
||||
return SlideTransition(position: offsetAnimation, child: child);
|
||||
},
|
||||
));
|
||||
} catch (error) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Fehler beim Laden des Items: $error")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
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);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xFF2B2B2B),
|
||||
foregroundColor: Colors.white,
|
||||
title: Text('f0ck #${widget.details.id}'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
if (widget.details.mime.contains(RegExp(r'image/(jpeg|png|gif|webp)'))) ...[
|
||||
Image.network(widget.details.mediaUrl),
|
||||
] else ...[
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
if (isAudio)
|
||||
Image.network('https://f0ck.me/ca/${widget.details.id}.webp'),
|
||||
FutureBuilder(
|
||||
future: _initializeVideoPlayer,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done && _controller.value.isInitialized) {
|
||||
return AspectRatio(
|
||||
aspectRatio: isAudio ? 1 : _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: VideoProgressIndicator(
|
||||
_controller,
|
||||
allowScrubbing: true,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.replay_10),
|
||||
onPressed: () {
|
||||
_controller.seekTo(_controller.value.position - const Duration(seconds: 10));
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_controller.value.isPlaying ? _controller.pause() : _controller.play();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.forward_10),
|
||||
onPressed: () {
|
||||
_controller.seekTo(_controller.value.position + const Duration(seconds: 10));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.details.mime}', style: const TextStyle(fontSize: 16, color: Colors.white)),
|
||||
Text("Benutzername: ${widget.details.username}", style: const TextStyle(fontSize: 16, color: Colors.white)),
|
||||
Text("Zeitstempel: ${widget.details.stamp}", style: const TextStyle(fontSize: 16, color: Colors.white)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
33
lib/services/api.dart
Normal file
33
lib/services/api.dart
Normal file
@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:f0ckapp/models/mediaitem.dart';
|
||||
import 'package:f0ckapp/models/mediaitem_detail.dart';
|
||||
|
||||
Future<List<MediaItem>> fetchMedia({String? older}) async {
|
||||
final Uri url = Uri.parse('https://f0ck.me/api/v2/items/get')
|
||||
.replace(queryParameters: {
|
||||
if (older != null) 'older': older,
|
||||
});
|
||||
|
||||
final response = await http.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
|
||||
final List<dynamic> jsonList = jsonResponse['items'];
|
||||
return jsonList.map((item) => MediaItem.fromJson(item)).toList();
|
||||
} else {
|
||||
throw Exception('Fehler beim Abrufen der Medien: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<MediaItemDetail> fetchMediaDetail(int itemId) async {
|
||||
final Uri url = Uri.parse('https://f0ck.me/api/v2/item/$itemId');
|
||||
|
||||
final response = await http.get(url);
|
||||
if (response.statusCode == 200) {
|
||||
final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
|
||||
return MediaItemDetail.fromJson(jsonResponse['rows']);
|
||||
} else {
|
||||
throw Exception('Fehler beim Abrufen der Media-Details: ${response.statusCode}');
|
||||
}
|
||||
}
|
51
lib/widgets/video_widget.dart
Normal file
51
lib/widgets/video_widget.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'package:f0ckapp/models/mediaitem_detail.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoWidget extends StatefulWidget {
|
||||
final MediaItemDetail details;
|
||||
//const VideoWidget({super.key, required this.details}): super(key: key);
|
||||
const VideoWidget({Key? key, required this.details}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoWidgetState createState() => _VideoWidgetState();
|
||||
}
|
||||
|
||||
class _VideoWidgetState extends State<VideoWidget> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.details.mediaUrl)
|
||||
)..initialize().then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
_controller.play();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: _controller.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
)
|
||||
: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
92
lib/widgets/video_widget.dart.old
Normal file
92
lib/widgets/video_widget.dart.old
Normal file
@ -0,0 +1,92 @@
|
||||
import 'package:f0ckapp/models/mediaitem_detail.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
class VideoWidget extends StatefulWidget {
|
||||
final MediaItemDetail details;
|
||||
const VideoWidget({super.key, required this.details});
|
||||
|
||||
@override
|
||||
State createState() => _VideoWidgetState();
|
||||
}
|
||||
|
||||
class _VideoWidgetState extends State<VideoWidget> {
|
||||
late final Player _player;
|
||||
late final VideoController _controller;
|
||||
double? _aspectRatio;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player = Player();
|
||||
_controller = VideoController(_player);
|
||||
_player.open(Media(widget.details.mediaUrl));
|
||||
_player.setPlaylistMode(PlaylistMode.loop);
|
||||
_player.setVolume(0);
|
||||
|
||||
//_player.stream.height.first;
|
||||
_player.stream.playing.listen((blah) {
|
||||
setState(() {
|
||||
Future.microtask(() async {
|
||||
int h = await _player.stream.height.first ?? 0;
|
||||
int w = await _player.stream.width.first ?? 0;
|
||||
_aspectRatio = h / w;
|
||||
print(_aspectRatio);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_player.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//_aspectRatio = _controller.player.state.height! / _controller.player.state.width!;
|
||||
print(_aspectRatio);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
|
||||
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
|
||||
title: Text('f0ck #${widget.details.id}'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (_aspectRatio != null)
|
||||
AspectRatio(
|
||||
aspectRatio: _aspectRatio!,
|
||||
child: Video(
|
||||
controller: _controller,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.of(context).size.height * 0.75,
|
||||
child: Video(
|
||||
controller: _controller,
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'f0cked by: ${widget.details.username}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Color.fromARGB(255, 255, 255, 255),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user