v1.0.26+26
All checks were successful
Flutter Schmutter / build (push) Successful in 3m20s

This commit is contained in:
2025-06-05 19:53:25 +02:00
parent bf77ccf8e3
commit 0d9ed1e6c1
9 changed files with 465 additions and 425 deletions

View File

@ -1,9 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.dart';
import 'package:f0ckapp/screens/DetailView.dart';
import 'dart:async';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
class MediaGrid extends StatefulWidget {
const MediaGrid({super.key});
@ -14,256 +14,175 @@ class MediaGrid extends StatefulWidget {
class _MediaGridState extends State<MediaGrid> {
final ScrollController _scrollController = ScrollController();
final String _version = '1.0.25+25';
List<MediaItem> mediaItems = [];
bool isLoading = false;
Timer? _debounceTimer;
Completer<void>? _navigationCompleter;
int _crossAxisCount = 0;
String _selectedType = 'alles';
int _selectedMode = 0;
bool _random = false;
final List<String> _modes = ["sfw", "nsfw", "untagged", "all"];
String? _selectedTag;
@override
void initState() {
super.initState();
_loadMedia();
final provider = Provider.of<MediaProvider>(context, listen: false);
Future.microtask(() {
provider.loadMedia();
});
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
_debounceLoadMedia();
provider.loadMedia();
}
});
}
void _debounceLoadMedia() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), _loadMedia);
}
int _calculateCrossAxisCount(BuildContext context) {
if (_crossAxisCount != 0) {
return _crossAxisCount;
}
double screenWidth = MediaQuery.of(context).size.width;
int columnCount = (screenWidth / 110).clamp(3, 5).toInt();
return columnCount;
}
Future<void> _loadMedia() async {
if (isLoading) return;
setState(() => isLoading = true);
try {
final newMedia = await fetchMedia(
older: mediaItems.isNotEmpty ? mediaItems.last.id.toString() : null,
type: _selectedType,
mode: _selectedMode,
random: _random,
tag: _selectedTag,
);
if (mounted) {
setState(() => mediaItems.addAll(newMedia));
}
} catch (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,
type: _selectedType,
mode: _selectedMode,
random: _random,
tag: _selectedTag,
);
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 {
if (_navigationCompleter?.isCompleted == false) return;
_navigationCompleter = Completer();
try {
if (mounted) {
final String? newTag = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailView(
initialItemId: item.id,
mediaItems: mediaItems,
type: _selectedType,
mode: _selectedMode,
random: _random,
tagname: _selectedTag,
),
),
);
if (newTag != null) {
setState(() {
if (newTag == '___empty___') {
_selectedTag = null;
}
else {
_selectedTag = newTag;
}
_refreshMedia();
});
}
}
} 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) {
final provider = Provider.of<MediaProvider>(context);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
centerTitle: true,
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
//centerTitle: true,
title: Text('f0ck v1.0.26+26'),
actions: [
DropdownButton<String>(
// mode
value: provider.modes[provider.mode],
isDense: true,
icon: SizedBox.shrink(),
items: provider.modes.map((String value) {
return DropdownMenuItem<String>(value: value, child: Text(value));
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
provider.setMode(provider.modes.indexOf(newValue));
}
},
),
IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
scaffoldKey.currentState?.openEndDrawer();
},
),
],
),
endDrawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
Text('f0ck v$_version'),
Checkbox(
value: _random,
onChanged: (bool? value) {
setState(() {
_random = !_random;
_refreshMedia();
});
DrawerHeader(
padding: EdgeInsets.all(0),
child: Image.asset('assets/images/menu.webp', fit: BoxFit.cover),
),
ListTile(
title: Text(
'All',
style: TextStyle(
fontWeight: provider.type == 'alles'
? FontWeight.bold
: FontWeight.normal,
color: provider.type == 'alles' ? Colors.blue : Colors.white,
),
),
onTap: () {
provider.setType('all');
},
),
ListTile(
title: Text(
'Images',
style: TextStyle(
fontWeight: provider.type == 'image'
? FontWeight.bold
: FontWeight.normal,
color: provider.type == 'image' ? Colors.blue : Colors.white,
),
),
onTap: () {
provider.setType('image');
},
),
ListTile(
title: Text(
'Videos',
style: TextStyle(
fontWeight: provider.type == 'video'
? FontWeight.bold
: FontWeight.normal,
color: provider.type == 'video' ? Colors.blue : Colors.white,
),
),
onTap: () {
provider.setType('video');
},
),
ListTile(
title: Text(
'Audio',
style: TextStyle(
fontWeight: provider.type == 'audio'
? FontWeight.bold
: FontWeight.normal,
color: provider.type == 'audio' ? Colors.blue : Colors.white,
),
),
onTap: () {
provider.setType('audio');
},
),
],
),
),
bottomNavigationBar: BottomAppBar(
color: const Color.fromARGB(255, 43, 43, 43),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
DropdownButton<String>(
value: _selectedType,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: ["alles", "image", "video", "audio"].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedType = newValue;
_refreshMedia();
});
}
},
persistentFooterButtons: provider.tag != null
? [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('tag: '),
InputChip(
label: Text(provider.tag!),
backgroundColor: const Color(0xFF090909),
labelStyle: const TextStyle(color: Colors.white),
onDeleted: () {
provider.setTag(null);
},
),
],
),
DropdownButton<String>(
value: _modes[_selectedMode],
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: _modes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedMode = _modes.indexOf(newValue);
_refreshMedia();
});
}
},
),
DropdownButton<int>(
value: _crossAxisCount,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: [0, 3, 4].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
value == 0 ? 'auto' : '$value Spalten',
style: TextStyle(color: Colors.white),
),
);
}).toList(),
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_crossAxisCount = newValue;
});
}
},
),
],
),
),
),
body: Stack(
children: [
RefreshIndicator(
onRefresh: _refreshMedia,
child: GridView.builder(
]
: null,
body: RefreshIndicator(
onRefresh: () async {
await provider.loadMedia(reload: true);
},
child: Consumer<MediaProvider>(
builder: (context, mediaProvider, child) {
return GridView.builder(
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _calculateCrossAxisCount(context),
crossAxisCount: mediaProvider.crossAxisCount == 0
? (MediaQuery.of(context).size.width / 110)
.clamp(3, 5)
.toInt()
: mediaProvider.crossAxisCount,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount: mediaItems.length + (isLoading ? 1 : 0),
itemCount:
provider.mediaItems.length + (provider.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= mediaItems.length) {
if (index >= provider.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaItems[index];
final item = provider.mediaItems[index];
return InkWell(
onTap: () => _navigateToDetail(item),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailView(initialItemId: item.id),
),
),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
@ -289,67 +208,10 @@ class _MediaGridState extends State<MediaGrid> {
),
);
},
),
),
if (_selectedTag != null)
Positioned(
bottom: 20,
left: MediaQuery.of(context).size.width * 0.2,
right: MediaQuery.of(context).size.width * 0.2,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 20,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Tag: $_selectedTag',
style: const TextStyle(color: Colors.white),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () {
setState(() {
_selectedTag = null;
_refreshMedia();
});
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
);
},
),
],
),
),
),
],
);
},
),
),
/*floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black.withValues(alpha: 0.8),
child: const Icon(Icons.arrow_upward, color: Colors.white),
onPressed: () {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOut,
);
},
),*/
);
}
@override
void dispose() {
_scrollController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
}