This commit is contained in:
@ -6,6 +6,13 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:encrypt_shared_preferences/provider.dart';
|
import 'package:encrypt_shared_preferences/provider.dart';
|
||||||
|
|
||||||
|
const Map<String, Locale> supportedLocales = {
|
||||||
|
'en_US': Locale('en', 'US'),
|
||||||
|
'de_DE': Locale('de', 'DE'),
|
||||||
|
'fr_FR': Locale('fr', 'FR'),
|
||||||
|
'nl_NL': Locale('nl', 'NL'),
|
||||||
|
};
|
||||||
|
|
||||||
class MyTranslations extends Translations {
|
class MyTranslations extends Translations {
|
||||||
static final MyTranslations instance = MyTranslations._internal();
|
static final MyTranslations instance = MyTranslations._internal();
|
||||||
MyTranslations._internal();
|
MyTranslations._internal();
|
||||||
@ -13,15 +20,18 @@ class MyTranslations extends Translations {
|
|||||||
static final Map<String, Map<String, String>> _translations = {};
|
static final Map<String, Map<String, String>> _translations = {};
|
||||||
|
|
||||||
static Future<void> loadTranslations() async {
|
static Future<void> loadTranslations() async {
|
||||||
final locales = ['en_US', 'de_DE', 'fr_FR', 'nl_NL'];
|
for (final localeKey in supportedLocales.keys) {
|
||||||
for (final locale in locales) {
|
try {
|
||||||
final String jsonString = await rootBundle.loadString(
|
final String jsonString = await rootBundle.loadString(
|
||||||
'assets/i18n/$locale.json',
|
'assets/i18n/$localeKey.json',
|
||||||
);
|
);
|
||||||
final Map<String, dynamic> jsonMap = json.decode(jsonString);
|
final Map<String, dynamic> jsonMap = json.decode(jsonString);
|
||||||
_translations[locale] = jsonMap.map(
|
_translations[localeKey] = jsonMap.map(
|
||||||
(key, value) => MapEntry(key, value.toString()),
|
(key, value) => MapEntry(key, value.toString()),
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Konnte Übersetzung für $localeKey nicht laden: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +42,7 @@ class MyTranslations extends Translations {
|
|||||||
class LocalizationController extends GetxController {
|
class LocalizationController extends GetxController {
|
||||||
final EncryptedSharedPreferencesAsync storage =
|
final EncryptedSharedPreferencesAsync storage =
|
||||||
EncryptedSharedPreferencesAsync.getInstance();
|
EncryptedSharedPreferencesAsync.getInstance();
|
||||||
Rx<Locale> currentLocale = const Locale('en', 'US').obs;
|
Rx<Locale> currentLocale = supportedLocales['en_US']!.obs;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -41,25 +51,29 @@ class LocalizationController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadLocale() async {
|
Future<void> loadLocale() async {
|
||||||
String? savedLocale = await storage.getString(
|
String? savedLocaleKey = await storage.getString(
|
||||||
'locale',
|
'locale',
|
||||||
defaultValue: 'en_US',
|
defaultValue: 'en_US',
|
||||||
);
|
);
|
||||||
if (savedLocale != null && savedLocale.isNotEmpty) {
|
|
||||||
final List<String> parts = savedLocale.split('_');
|
final Locale locale =
|
||||||
currentLocale.value = parts.length == 2
|
supportedLocales[savedLocaleKey ?? 'en_US'] ??
|
||||||
? Locale(parts[0], parts[1])
|
supportedLocales['en_US']!;
|
||||||
: Locale(parts[0]);
|
|
||||||
Get.locale = currentLocale.value;
|
currentLocale.value = locale;
|
||||||
}
|
Get.locale = locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> changeLocale(Locale newLocale) async {
|
Future<void> changeLocale(Locale newLocale) async {
|
||||||
|
final String localeKey = supportedLocales.entries
|
||||||
|
.firstWhere(
|
||||||
|
(entry) => entry.value == newLocale,
|
||||||
|
orElse: () => supportedLocales.entries.first,
|
||||||
|
)
|
||||||
|
.key;
|
||||||
|
|
||||||
currentLocale.value = newLocale;
|
currentLocale.value = newLocale;
|
||||||
Get.updateLocale(newLocale);
|
Get.updateLocale(newLocale);
|
||||||
await storage.setString(
|
await storage.setString('locale', localeKey);
|
||||||
'locale',
|
|
||||||
'${newLocale.languageCode}_${newLocale.countryCode}',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
|||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||||
|
|
||||||
class MediaController extends GetxController {
|
class MediaController extends GetxController {
|
||||||
final ApiService _api = ApiService();
|
final ApiService _api = Get.find<ApiService>();
|
||||||
final EncryptedSharedPreferencesAsync storage =
|
final EncryptedSharedPreferencesAsync storage =
|
||||||
EncryptedSharedPreferencesAsync.getInstance();
|
EncryptedSharedPreferencesAsync.getInstance();
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ class MediaController extends GetxController {
|
|||||||
RxInt typeIndex = 0.obs;
|
RxInt typeIndex = 0.obs;
|
||||||
RxInt modeIndex = 0.obs;
|
RxInt modeIndex = 0.obs;
|
||||||
RxInt random = 0.obs;
|
RxInt random = 0.obs;
|
||||||
Rxn<String> tag = Rxn<String>();
|
Rxn<String> tag = Rxn<String>(null);
|
||||||
RxBool muted = false.obs;
|
RxBool muted = false.obs;
|
||||||
Rx<PageTransition> transitionType = PageTransition.opacity.obs;
|
Rx<PageTransition> transitionType = PageTransition.opacity.obs;
|
||||||
RxBool drawerSwipeEnabled = true.obs;
|
RxBool drawerSwipeEnabled = true.obs;
|
||||||
@ -45,14 +45,6 @@ class MediaController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MediaItem> get filteredItems {
|
|
||||||
final String typeStr = mediaTypes[typeIndex.value];
|
|
||||||
return items.where((item) {
|
|
||||||
final bool typeOk = typeStr == "alles" || item.mime.startsWith(typeStr);
|
|
||||||
return typeOk;
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Favorite>?> toggleFavorite(
|
Future<List<Favorite>?> toggleFavorite(
|
||||||
MediaItem item,
|
MediaItem item,
|
||||||
bool isFavorite,
|
bool isFavorite,
|
||||||
@ -64,68 +56,82 @@ class MediaController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchInitial({int? id}) async {
|
Future<Feed?> _fetchItems({int? older, int? newer}) async {
|
||||||
|
if (loading.value) return null;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
final Feed result = await _api.fetchItems(
|
return await _api.fetchItems(
|
||||||
|
older: older,
|
||||||
|
newer: newer,
|
||||||
type: typeIndex.value,
|
type: typeIndex.value,
|
||||||
mode: modeIndex.value,
|
mode: modeIndex.value,
|
||||||
random: random.value,
|
random: random.value,
|
||||||
tag: tag.value,
|
tag: tag.value,
|
||||||
older: id,
|
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Fehler beim Laden',
|
||||||
|
'Die Daten konnten nicht abgerufen werden. Wo Internet?',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchInitial({int? id}) async {
|
||||||
|
final result = await _fetchItems(older: id);
|
||||||
|
if (result != null) {
|
||||||
items.assignAll(result.items);
|
items.assignAll(result.items);
|
||||||
atEnd.value = result.atEnd;
|
atEnd.value = result.atEnd;
|
||||||
atStart.value = result.atStart;
|
atStart.value = result.atStart;
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fetchMore() async {
|
Future<void> fetchMore() async {
|
||||||
if (items.isEmpty || atEnd.value) return;
|
if (items.isEmpty || atEnd.value) return;
|
||||||
loading.value = true;
|
final result = await _fetchItems(older: items.last.id);
|
||||||
try {
|
if (result != null) {
|
||||||
final Feed result = await _api.fetchItems(
|
final Set<int> existingIds = items.map((e) => e.id).toSet();
|
||||||
older: items.last.id,
|
|
||||||
type: typeIndex.value,
|
|
||||||
mode: modeIndex.value,
|
|
||||||
random: random.value,
|
|
||||||
tag: tag.value,
|
|
||||||
);
|
|
||||||
final List<MediaItem> newItems = result.items
|
final List<MediaItem> newItems = result.items
|
||||||
.where((item) => !items.any((existing) => existing.id == item.id))
|
.where((item) => !existingIds.contains(item.id))
|
||||||
.toList();
|
.toList();
|
||||||
items.addAll(newItems);
|
items.addAll(newItems);
|
||||||
items.refresh();
|
items.refresh();
|
||||||
atEnd.value = result.atEnd;
|
atEnd.value = result.atEnd;
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> fetchNewer() async {
|
Future<int> fetchNewer() async {
|
||||||
if (items.isEmpty || atStart.value) return 0;
|
if (items.isEmpty || atStart.value) return 0;
|
||||||
loading.value = true;
|
final oldLength = items.length;
|
||||||
try {
|
final result = await _fetchItems(newer: items.first.id);
|
||||||
final Feed result = await _api.fetchItems(
|
if (result != null) {
|
||||||
newer: items.first.id,
|
final Set<int> existingIds = items.map((e) => e.id).toSet();
|
||||||
type: typeIndex.value,
|
|
||||||
mode: modeIndex.value,
|
|
||||||
random: random.value,
|
|
||||||
tag: tag.value,
|
|
||||||
);
|
|
||||||
int oldLength = filteredItems.length;
|
|
||||||
final List<MediaItem> newItems = result.items
|
final List<MediaItem> newItems = result.items
|
||||||
.where((item) => !items.any((existing) => existing.id == item.id))
|
.where((item) => !existingIds.contains(item.id))
|
||||||
.toList();
|
.toList();
|
||||||
items.insertAll(0, newItems);
|
items.insertAll(0, newItems);
|
||||||
items.refresh();
|
items.refresh();
|
||||||
atStart.value = result.atStart;
|
atStart.value = result.atStart;
|
||||||
int newLength = filteredItems.length;
|
return items.length - oldLength;
|
||||||
return newLength - oldLength;
|
}
|
||||||
} finally {
|
return 0;
|
||||||
loading.value = false;
|
}
|
||||||
|
|
||||||
|
Future<void> handleRefresh() async {
|
||||||
|
if (loading.value) return;
|
||||||
|
if (!atStart.value) {
|
||||||
|
await fetchNewer();
|
||||||
|
} else {
|
||||||
|
await fetchInitial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> handleLoading() async {
|
||||||
|
if (!loading.value && !atEnd.value) {
|
||||||
|
await fetchMore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,4 +184,6 @@ class MediaController extends GetxController {
|
|||||||
await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
|
await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
|
||||||
await storage.setInt('transitionType', transitionType.value.index);
|
await storage.setInt('transitionType', transitionType.value.index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get isRandomEnabled => random.value == 1;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:encrypt_shared_preferences/provider.dart';
|
import 'package:encrypt_shared_preferences/provider.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/services/api.dart';
|
||||||
import 'package:f0ckapp/controller/authcontroller.dart';
|
import 'package:f0ckapp/controller/authcontroller.dart';
|
||||||
import 'package:f0ckapp/controller/localizationcontroller.dart';
|
import 'package:f0ckapp/controller/localizationcontroller.dart';
|
||||||
import 'package:f0ckapp/controller/themecontroller.dart';
|
import 'package:f0ckapp/controller/themecontroller.dart';
|
||||||
@ -14,11 +15,17 @@ import 'package:f0ckapp/screens/login.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
|
|
||||||
await MyTranslations.loadTranslations();
|
await Future.wait([
|
||||||
await AppVersion.init();
|
EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9'),
|
||||||
|
MyTranslations.loadTranslations(),
|
||||||
|
AppVersion.init(),
|
||||||
|
]);
|
||||||
|
|
||||||
Get.put(AuthController());
|
Get.put(AuthController());
|
||||||
final MediaController mediaController = Get.put(MediaController());
|
Get.put(ApiService());
|
||||||
|
Get.put(MediaController());
|
||||||
|
|
||||||
final ThemeController themeController = Get.put(ThemeController());
|
final ThemeController themeController = Get.put(ThemeController());
|
||||||
final LocalizationController localizationController = Get.put(
|
final LocalizationController localizationController = Get.put(
|
||||||
LocalizationController(),
|
LocalizationController(),
|
||||||
@ -41,7 +48,7 @@ void main() async {
|
|||||||
final Uri uri = Uri.parse(settings.name ?? '/');
|
final Uri uri = Uri.parse(settings.name ?? '/');
|
||||||
|
|
||||||
if (uri.path == '/' || uri.pathSegments.isEmpty) {
|
if (uri.path == '/' || uri.pathSegments.isEmpty) {
|
||||||
return MaterialPageRoute(builder: (_) => MediaGrid());
|
return MaterialPageRoute(builder: (_) => const MediaGrid());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri.path == '/login') {
|
if (uri.path == '/login') {
|
||||||
@ -49,26 +56,17 @@ void main() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (uri.pathSegments.length == 1) {
|
if (uri.pathSegments.length == 1) {
|
||||||
final int id = int.parse(uri.pathSegments.first);
|
try {
|
||||||
|
final int id = int.parse(uri.pathSegments.first);
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) => FutureBuilder(
|
builder: (_) => MediaDetailScreen(initialId: id),
|
||||||
future: mediaController.items.isEmpty
|
);
|
||||||
? mediaController.fetchInitial(id: id)
|
} catch (e) {
|
||||||
: Future.value(),
|
return MaterialPageRoute(builder: (_) => const MediaGrid());
|
||||||
builder: (context, snapshot) {
|
}
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
|
||||||
return MediaDetailScreen(initialId: id);
|
|
||||||
}
|
|
||||||
return const Scaffold(
|
|
||||||
body: Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterialPageRoute(builder: (_) => MediaGrid());
|
return MaterialPageRoute(builder: (_) => const MediaGrid());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -50,7 +50,7 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
|
|||||||
const Icon(Icons.error),
|
const Icon(Icons.error),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox.expand(
|
: Center(
|
||||||
child: VideoWidget(
|
child: VideoWidget(
|
||||||
details: widget.item,
|
details: widget.item,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
@ -7,16 +7,19 @@ import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/widgets/tagfooter.dart';
|
||||||
import 'package:f0ckapp/utils/animatedtransition.dart';
|
import 'package:f0ckapp/utils/animatedtransition.dart';
|
||||||
import 'package:f0ckapp/controller/authcontroller.dart';
|
import 'package:f0ckapp/controller/authcontroller.dart';
|
||||||
import 'package:f0ckapp/widgets/actiontag.dart';
|
import 'package:f0ckapp/widgets/actiontag.dart';
|
||||||
import 'package:f0ckapp/widgets/favorite_avatars.dart';
|
import 'package:f0ckapp/widgets/favoritesection.dart';
|
||||||
import 'package:f0ckapp/screens/fullscreen.dart';
|
import 'package:f0ckapp/screens/fullscreen.dart';
|
||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
|
import 'package:f0ckapp/widgets/end_drawer.dart';
|
||||||
import 'package:f0ckapp/controller/mediacontroller.dart';
|
import 'package:f0ckapp/controller/mediacontroller.dart';
|
||||||
import 'package:f0ckapp/models/item.dart';
|
import 'package:f0ckapp/models/item.dart';
|
||||||
import 'package:f0ckapp/widgets/video_widget.dart';
|
import 'package:f0ckapp/widgets/video_widget.dart';
|
||||||
|
|
||||||
|
enum ShareAction { media, directLink, postLink }
|
||||||
|
|
||||||
class MediaDetailScreen extends StatefulWidget {
|
class MediaDetailScreen extends StatefulWidget {
|
||||||
final int initialId;
|
final int initialId;
|
||||||
const MediaDetailScreen({super.key, required this.initialId});
|
const MediaDetailScreen({super.key, required this.initialId});
|
||||||
@ -26,19 +29,70 @@ class MediaDetailScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
||||||
late PageController _pageController;
|
PageController? _pageController;
|
||||||
final MediaController mediaController = Get.find<MediaController>();
|
final MediaController mediaController = Get.find<MediaController>();
|
||||||
final AuthController authController = Get.find<AuthController>();
|
final AuthController authController = Get.find<AuthController>();
|
||||||
int? _currentIndex;
|
final _currentIndex = 0.obs;
|
||||||
|
final _mediaSaverChannel = const MethodChannel('MediaShit');
|
||||||
|
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _itemNotFound = false;
|
||||||
|
final Set<int> _readyItemIds = {};
|
||||||
|
|
||||||
|
final List<PopupMenuEntry<ShareAction>> _shareMenuItems = const [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: ShareAction.media,
|
||||||
|
child: ListTile(leading: Icon(Icons.image), title: Text('Als Datei')),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: ShareAction.directLink,
|
||||||
|
child: ListTile(leading: Icon(Icons.link), title: Text('Link zur Datei')),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: ShareAction.postLink,
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(Icons.article),
|
||||||
|
title: Text('Link zum f0ck'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final int idx = mediaController.items.indexWhere(
|
_loadInitialItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadInitialItem() async {
|
||||||
|
int initialIndex = mediaController.items.indexWhere(
|
||||||
(item) => item.id == widget.initialId,
|
(item) => item.id == widget.initialId,
|
||||||
);
|
);
|
||||||
_currentIndex = idx >= 0 ? idx : 0;
|
|
||||||
_pageController = PageController(initialPage: _currentIndex!);
|
if (initialIndex < 0) {
|
||||||
|
await mediaController.fetchInitial(id: widget.initialId + 20);
|
||||||
|
initialIndex = mediaController.items.indexWhere(
|
||||||
|
(item) => item.id == widget.initialId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialIndex < 0) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_itemNotFound = true;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
_currentIndex.value = initialIndex;
|
||||||
|
_pageController = PageController(initialPage: initialIndex);
|
||||||
|
if (mediaController.items[initialIndex].mime.startsWith('image/')) {
|
||||||
|
_readyItemIds.add(mediaController.items[initialIndex].id);
|
||||||
|
}
|
||||||
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showMsg(String message) {
|
void _showMsg(String message) {
|
||||||
@ -49,15 +103,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onPageChanged(int idx) {
|
void _onPageChanged(int idx) {
|
||||||
if (idx != _currentIndex) {
|
if (idx != _currentIndex.value) {
|
||||||
setState(() => _currentIndex = idx);
|
_currentIndex.value = idx;
|
||||||
|
final item = mediaController.items[idx];
|
||||||
|
if (item.mime.startsWith('image/') && !_readyItemIds.contains(item.id)) {
|
||||||
|
setState(() => _readyItemIds.add(item.id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (idx >= mediaController.items.length - 2 &&
|
if (idx >= mediaController.items.length - 2 &&
|
||||||
!mediaController.loading.value &&
|
!mediaController.loading.value &&
|
||||||
!mediaController.atEnd.value) {
|
!mediaController.atEnd.value) {
|
||||||
mediaController.fetchMore();
|
mediaController.fetchMore();
|
||||||
}
|
} else if (idx <= 1 &&
|
||||||
if (idx <= 1 &&
|
|
||||||
!mediaController.loading.value &&
|
!mediaController.loading.value &&
|
||||||
!mediaController.atStart.value) {
|
!mediaController.atStart.value) {
|
||||||
mediaController.fetchNewer();
|
mediaController.fetchNewer();
|
||||||
@ -65,22 +122,58 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadMedia(MediaItem item) async {
|
Future<void> _downloadMedia(MediaItem item) async {
|
||||||
final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
|
try {
|
||||||
final MethodChannel methodChannel = const MethodChannel('MediaShit');
|
final File file = await DefaultCacheManager().getSingleFile(
|
||||||
|
item.mediaUrl,
|
||||||
|
);
|
||||||
|
|
||||||
bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
|
final bool? success = await _mediaSaverChannel.invokeMethod<bool>(
|
||||||
'filePath': file.path,
|
'saveFile',
|
||||||
'fileName': item.dest,
|
{'filePath': file.path, 'fileName': item.dest},
|
||||||
});
|
);
|
||||||
|
|
||||||
success == true
|
success == true
|
||||||
? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
|
? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
|
||||||
: _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
|
: _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
|
||||||
|
} catch (e) {
|
||||||
|
_showMsg('Fehler beim Download: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleShareAction(ShareAction value, MediaItem item) async {
|
||||||
|
try {
|
||||||
|
if (value == ShareAction.media) {
|
||||||
|
final File file = await DefaultCacheManager().getSingleFile(
|
||||||
|
item.mediaUrl,
|
||||||
|
);
|
||||||
|
final Uint8List bytes = await file.readAsBytes();
|
||||||
|
final params = ShareParams(
|
||||||
|
files: [XFile.fromData(bytes, mimeType: item.mime)],
|
||||||
|
);
|
||||||
|
await SharePlus.instance.share(params);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String textToShare;
|
||||||
|
switch (value) {
|
||||||
|
case ShareAction.directLink:
|
||||||
|
textToShare = item.mediaUrl;
|
||||||
|
break;
|
||||||
|
case ShareAction.postLink:
|
||||||
|
textToShare = item.postUrl;
|
||||||
|
break;
|
||||||
|
case ShareAction.media:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await SharePlus.instance.share(ShareParams(text: textToShare));
|
||||||
|
} catch (e) {
|
||||||
|
_showMsg('Fehler beim Teilen: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pageController.dispose();
|
_pageController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +186,15 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
|||||||
);
|
);
|
||||||
} else if (item.mime.startsWith('video/') ||
|
} else if (item.mime.startsWith('video/') ||
|
||||||
item.mime.startsWith('audio/')) {
|
item.mime.startsWith('audio/')) {
|
||||||
return VideoWidget(details: item, isActive: isActive);
|
return VideoWidget(
|
||||||
|
details: item,
|
||||||
|
isActive: isActive,
|
||||||
|
onInitialized: () {
|
||||||
|
if (mounted && !_readyItemIds.contains(item.id)) {
|
||||||
|
setState(() => _readyItemIds.add(item.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return const Icon(Icons.help_outline, size: 100);
|
return const Icon(Icons.help_outline, size: 100);
|
||||||
}
|
}
|
||||||
@ -101,209 +202,134 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text('Lade f0ck #${widget.initialId}...')),
|
||||||
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_itemNotFound) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Fehler')),
|
||||||
|
body: const Center(child: Text('f0ck nicht gefunden.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Obx(
|
return Obx(
|
||||||
() => PageView.builder(
|
() => PageView.builder(
|
||||||
controller: _pageController,
|
controller: _pageController!,
|
||||||
itemCount: mediaController.items.length,
|
itemCount: mediaController.items.length,
|
||||||
onPageChanged: _onPageChanged,
|
onPageChanged: _onPageChanged,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final MediaItem item = mediaController.items[index];
|
final MediaItem item = mediaController.items[index];
|
||||||
final bool isActive = index == _currentIndex;
|
final bool isReady = _readyItemIds.contains(item.id);
|
||||||
final bool isFavorite =
|
|
||||||
item.favorites?.any(
|
|
||||||
(f) => f.userId == authController.userId.value,
|
|
||||||
) ??
|
|
||||||
false;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
endDrawer: EndDrawer(),
|
endDrawer: EndDrawer(),
|
||||||
endDrawerEnableOpenDragGesture:
|
endDrawerEnableOpenDragGesture:
|
||||||
mediaController.drawerSwipeEnabled.value,
|
mediaController.drawerSwipeEnabled.value,
|
||||||
body: CustomScrollView(
|
appBar: AppBar(
|
||||||
slivers: [
|
title: Text('f0ck #${item.id}'),
|
||||||
SliverAppBar(
|
actions: [
|
||||||
floating: false,
|
IconButton(
|
||||||
pinned: true,
|
icon: const Icon(Icons.fullscreen),
|
||||||
title: Text('f0ck #${item.id}'),
|
onPressed: () {
|
||||||
actions: [
|
Get.to(
|
||||||
IconButton(
|
FullScreenMediaView(item: item),
|
||||||
icon: const Icon(Icons.fullscreen),
|
fullscreenDialog: true,
|
||||||
onPressed: () {
|
);
|
||||||
Get.to(
|
},
|
||||||
FullScreenMediaView(item: item),
|
|
||||||
fullscreenDialog: true,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.download),
|
|
||||||
onPressed: () async {
|
|
||||||
await _downloadMedia(item);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuButton<String>(
|
|
||||||
onSelected: (value) async {
|
|
||||||
switch (value) {
|
|
||||||
case 'media':
|
|
||||||
File file = await DefaultCacheManager()
|
|
||||||
.getSingleFile(item.mediaUrl);
|
|
||||||
Uint8List bytes = await file.readAsBytes();
|
|
||||||
final params = ShareParams(
|
|
||||||
files: [
|
|
||||||
XFile.fromData(bytes, mimeType: item.mime),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
await SharePlus.instance.share(params);
|
|
||||||
break;
|
|
||||||
case 'direct_link':
|
|
||||||
await SharePlus.instance.share(
|
|
||||||
ShareParams(text: item.mediaUrl),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'post_link':
|
|
||||||
await SharePlus.instance.share(
|
|
||||||
ShareParams(text: item.postUrl),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'media',
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.image),
|
|
||||||
title: const Text('Als Datei'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'direct_link',
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.link),
|
|
||||||
title: const Text('Link zur Datei'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'post_link',
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.article),
|
|
||||||
title: const Text('Link zum f0ck'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
),
|
|
||||||
Builder(
|
|
||||||
builder: (context) => IconButton(
|
|
||||||
icon: const Icon(Icons.menu),
|
|
||||||
onPressed: () {
|
|
||||||
Scaffold.of(context).openEndDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
IconButton(
|
||||||
child: Column(
|
icon: const Icon(Icons.download),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
onPressed: () async {
|
||||||
children: [
|
await _downloadMedia(item);
|
||||||
AnimatedBuilder(
|
},
|
||||||
animation: _pageController,
|
),
|
||||||
builder: (context, child) {
|
PopupMenuButton<ShareAction>(
|
||||||
return buildAnimatedTransition(
|
onSelected: (value) => _handleShareAction(value, item),
|
||||||
context: context,
|
itemBuilder: (context) => _shareMenuItems,
|
||||||
pageController: _pageController,
|
icon: const Icon(Icons.share),
|
||||||
index: index,
|
),
|
||||||
controller: mediaController,
|
Builder(
|
||||||
child: child!,
|
builder: (context) => IconButton(
|
||||||
);
|
icon: const Icon(Icons.menu),
|
||||||
},
|
onPressed: () {
|
||||||
child: _buildMedia(item, isActive),
|
Scaffold.of(context).openEndDrawer();
|
||||||
),
|
},
|
||||||
const SizedBox(height: 16),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
...item.tags?.map(
|
|
||||||
(tag) => Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 6),
|
|
||||||
child: ActionTag(tag, (onTagTap) {
|
|
||||||
if (tag.tag == 'sfw' || tag.tag == 'nsfw') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mediaController.setTag(onTagTap);
|
|
||||||
Get.offAllNamed('/');
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
) ?? [],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (authController.isLoggedIn) ...[
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: FavoriteAvatars(
|
|
||||||
favorites: item.favorites ?? [],
|
|
||||||
brightness: Theme.of(
|
|
||||||
context,
|
|
||||||
).brightness,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: isFavorite
|
|
||||||
? const Icon(Icons.favorite)
|
|
||||||
: const Icon(Icons.favorite_outline),
|
|
||||||
color: Colors.red,
|
|
||||||
onPressed: () async {
|
|
||||||
final List<Favorite>? newFavorites =
|
|
||||||
await mediaController.toggleFavorite(
|
|
||||||
item,
|
|
||||||
isFavorite,
|
|
||||||
);
|
|
||||||
if (newFavorites != null) {
|
|
||||||
mediaController.items[index] = item
|
|
||||||
.copyWith(favorites: newFavorites);
|
|
||||||
mediaController.items.refresh();
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
persistentFooterButtons: [
|
body: SingleChildScrollView(
|
||||||
Obx(() {
|
child: Column(
|
||||||
if (mediaController.tag.value != null) {
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
return Center(
|
children: [
|
||||||
child: InputChip(
|
AnimatedBuilder(
|
||||||
label: Text(mediaController.tag.value!),
|
animation: _pageController!,
|
||||||
onDeleted: () {
|
builder: (context, child) {
|
||||||
mediaController.setTag(null);
|
return buildAnimatedTransition(
|
||||||
Get.offAllNamed('/');
|
context: context,
|
||||||
},
|
pageController: _pageController!,
|
||||||
|
index: index,
|
||||||
|
controller: mediaController,
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Obx(
|
||||||
|
() => _buildMedia(item, index == _currentIndex.value),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
} else {
|
const SizedBox(height: 16),
|
||||||
return SizedBox.shrink();
|
if (isReady)
|
||||||
}
|
Padding(
|
||||||
}),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
],
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 6.0,
|
||||||
|
runSpacing: 4.0,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
...item.tags?.map(
|
||||||
|
(tag) => ActionTag(
|
||||||
|
tag,
|
||||||
|
(tag.tag == 'sfw' || tag.tag == 'nsfw')
|
||||||
|
? (onTagTap) => {}
|
||||||
|
: (onTagTap) {
|
||||||
|
mediaController.setTag(onTagTap);
|
||||||
|
Get.offAllNamed('/');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => Visibility(
|
||||||
|
visible: authController.isLoggedIn,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
|
child: FavoriteSection(
|
||||||
|
item: item,
|
||||||
|
index: index,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
persistentFooterButtons: mediaController.tag.value != null
|
||||||
|
? [TagFooter()]
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:pullex/pullex.dart';
|
import 'package:pullex/pullex.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/widgets/tagfooter.dart';
|
||||||
import 'package:f0ckapp/utils/customsearchdelegate.dart';
|
import 'package:f0ckapp/utils/customsearchdelegate.dart';
|
||||||
import 'package:f0ckapp/widgets/end_drawer.dart';
|
import 'package:f0ckapp/widgets/end_drawer.dart';
|
||||||
import 'package:f0ckapp/widgets/filter_bar.dart';
|
import 'package:f0ckapp/widgets/filter_bar.dart';
|
||||||
import 'package:f0ckapp/screens/mediadetail.dart';
|
|
||||||
import 'package:f0ckapp/widgets/media_tile.dart';
|
import 'package:f0ckapp/widgets/media_tile.dart';
|
||||||
import 'package:f0ckapp/controller/mediacontroller.dart';
|
import 'package:f0ckapp/controller/mediacontroller.dart';
|
||||||
|
|
||||||
@ -24,151 +24,162 @@ class _MediaGrid extends State<MediaGrid> {
|
|||||||
initialRefresh: false,
|
initialRefresh: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
late final _MediaGridAppBar _appBar;
|
||||||
|
late final _MediaGridBody _body;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_mediaController.fetchInitial();
|
_mediaController.fetchInitial();
|
||||||
|
_appBar = _MediaGridAppBar(mediaController: _mediaController);
|
||||||
|
_body = _MediaGridBody(
|
||||||
|
refreshController: _refreshController,
|
||||||
|
mediaController: _mediaController,
|
||||||
|
scrollController: _scrollController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
_refreshController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Obx(
|
||||||
endDrawer: EndDrawer(),
|
() => Scaffold(
|
||||||
endDrawerEnableOpenDragGesture: _mediaController.drawerSwipeEnabled.value,
|
endDrawer: const EndDrawer(),
|
||||||
bottomNavigationBar: FilterBar(scrollController: _scrollController),
|
endDrawerEnableOpenDragGesture:
|
||||||
body: PullexRefresh(
|
_mediaController.drawerSwipeEnabled.value,
|
||||||
controller: _refreshController,
|
bottomNavigationBar: FilterBar(scrollController: _scrollController),
|
||||||
enablePullDown: true,
|
appBar: _appBar,
|
||||||
enablePullUp: true,
|
body: _body,
|
||||||
header: MaterialHeader(offset: 140),
|
persistentFooterButtons: _mediaController.tag.value != null
|
||||||
onRefresh: () async {
|
? [TagFooter()]
|
||||||
try {
|
: null,
|
||||||
if (_mediaController.loading.value) return;
|
),
|
||||||
if (!_mediaController.atStart.value) {
|
);
|
||||||
await _mediaController.fetchNewer();
|
}
|
||||||
} else {
|
}
|
||||||
await _mediaController.fetchInitial();
|
|
||||||
}
|
class _MediaGridAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
} finally {
|
const _MediaGridAppBar({required this.mediaController});
|
||||||
_refreshController.refreshCompleted();
|
|
||||||
}
|
final MediaController mediaController;
|
||||||
},
|
|
||||||
onLoading: () async {
|
@override
|
||||||
try {
|
Widget build(BuildContext context) {
|
||||||
if (!_mediaController.loading.value &&
|
return AppBar(
|
||||||
!_mediaController.atEnd.value) {
|
title: InkWell(
|
||||||
await _mediaController.fetchMore();
|
onTap: () {
|
||||||
}
|
mediaController.setTag(null);
|
||||||
} finally {
|
},
|
||||||
_refreshController.loadComplete();
|
child: Row(
|
||||||
}
|
mainAxisSize: MainAxisSize.min,
|
||||||
},
|
children: [
|
||||||
child: CustomScrollView(
|
Image.asset('assets/images/f0ck_small.webp', fit: BoxFit.fitHeight),
|
||||||
controller: _scrollController,
|
const SizedBox(width: 10),
|
||||||
slivers: [
|
const Text('fApp', style: TextStyle(fontSize: 24)),
|
||||||
SliverAppBar(
|
],
|
||||||
pinned: false,
|
),
|
||||||
snap: true,
|
),
|
||||||
floating: true,
|
actions: [
|
||||||
title: GestureDetector(
|
IconButton(
|
||||||
child: Row(
|
icon: const Icon(Icons.search),
|
||||||
children: [
|
onPressed: () async {
|
||||||
Image.asset(
|
await showSearch(
|
||||||
'assets/images/f0ck_small.webp',
|
context: context,
|
||||||
fit: BoxFit.fitHeight,
|
delegate: CustomSearchDelegate(),
|
||||||
),
|
);
|
||||||
const SizedBox(width: 10),
|
},
|
||||||
const Text('fApp', style: TextStyle(fontSize: 24)),
|
),
|
||||||
],
|
Obx(
|
||||||
),
|
() => IconButton(
|
||||||
onTap: () {
|
icon: Icon(
|
||||||
_mediaController.setTag(null);
|
mediaController.isRandomEnabled
|
||||||
},
|
? Icons.shuffle_on_outlined
|
||||||
),
|
: Icons.shuffle,
|
||||||
actions: [
|
),
|
||||||
IconButton(
|
onPressed: mediaController.toggleRandom,
|
||||||
icon: const Icon(Icons.search),
|
),
|
||||||
onPressed: () async {
|
),
|
||||||
await showSearch(
|
IconButton(
|
||||||
context: context,
|
icon: const Icon(Icons.menu),
|
||||||
delegate: CustomSearchDelegate(),
|
onPressed: () => Scaffold.of(context).openEndDrawer(),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
);
|
||||||
Obx(
|
}
|
||||||
() => IconButton(
|
|
||||||
icon: Icon(
|
@override
|
||||||
_mediaController.random.value == 1
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
? Icons.shuffle_on_outlined
|
}
|
||||||
: Icons.shuffle,
|
|
||||||
),
|
class _MediaGridBody extends StatelessWidget {
|
||||||
onPressed: () {
|
const _MediaGridBody({
|
||||||
_mediaController.toggleRandom();
|
required this.refreshController,
|
||||||
},
|
required this.mediaController,
|
||||||
),
|
required this.scrollController,
|
||||||
),
|
});
|
||||||
Builder(
|
|
||||||
builder: (context) {
|
final PullexRefreshController refreshController;
|
||||||
return IconButton(
|
final MediaController mediaController;
|
||||||
icon: const Icon(Icons.menu),
|
final ScrollController scrollController;
|
||||||
onPressed: () {
|
|
||||||
Scaffold.of(context).openEndDrawer();
|
@override
|
||||||
},
|
Widget build(BuildContext context) {
|
||||||
);
|
return PullexRefresh(
|
||||||
},
|
controller: refreshController,
|
||||||
),
|
enablePullDown: true,
|
||||||
],
|
enablePullUp: true,
|
||||||
),
|
header: const MaterialHeader(),
|
||||||
Obx(
|
onRefresh: () async {
|
||||||
() => SliverPadding(
|
try {
|
||||||
padding: const EdgeInsets.all(4),
|
await mediaController.handleRefresh();
|
||||||
sliver: SliverGrid(
|
} finally {
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
refreshController.refreshCompleted();
|
||||||
final item = _mediaController.filteredItems[index];
|
}
|
||||||
return GestureDetector(
|
},
|
||||||
onTap: () {
|
onLoading: () async {
|
||||||
final item = _mediaController.filteredItems[index];
|
try {
|
||||||
Get.to(() => MediaDetailScreen(initialId: item.id));
|
await mediaController.handleLoading();
|
||||||
},
|
} finally {
|
||||||
child: MediaTile(item: item),
|
refreshController.loadComplete();
|
||||||
);
|
}
|
||||||
}, childCount: _mediaController.filteredItems.length),
|
},
|
||||||
gridDelegate: _mediaController.crossAxisCount.value == 0
|
child: Obx(
|
||||||
? const SliverGridDelegateWithMaxCrossAxisExtent(
|
() => GridView.builder(
|
||||||
maxCrossAxisExtent: 150,
|
addAutomaticKeepAlives: false,
|
||||||
crossAxisSpacing: 5,
|
controller: scrollController,
|
||||||
mainAxisSpacing: 5,
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
childAspectRatio: 1,
|
shrinkWrap: true,
|
||||||
)
|
padding: const EdgeInsets.all(4),
|
||||||
: SliverGridDelegateWithFixedCrossAxisCount(
|
itemCount: mediaController.items.length,
|
||||||
crossAxisCount: _mediaController.crossAxisCount.value,
|
gridDelegate: mediaController.crossAxisCount.value == 0
|
||||||
crossAxisSpacing: 5,
|
? const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
mainAxisSpacing: 5,
|
maxCrossAxisExtent: 150,
|
||||||
childAspectRatio: 1,
|
crossAxisSpacing: 5,
|
||||||
),
|
mainAxisSpacing: 5,
|
||||||
),
|
childAspectRatio: 1,
|
||||||
),
|
)
|
||||||
),
|
: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
],
|
crossAxisCount: mediaController.crossAxisCount.value,
|
||||||
),
|
crossAxisSpacing: 5,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
childAspectRatio: 1,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = mediaController.items[index];
|
||||||
|
return GestureDetector(
|
||||||
|
key: ValueKey(item.id),
|
||||||
|
onTap: () => Get.toNamed('/${item.id}'),
|
||||||
|
child: MediaTile(item: item),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
persistentFooterButtons: [
|
|
||||||
Obx(() {
|
|
||||||
if (_mediaController.tag.value != null) {
|
|
||||||
return Center(
|
|
||||||
child: InputChip(
|
|
||||||
label: Text(_mediaController.tag.value!),
|
|
||||||
onDeleted: () {
|
|
||||||
_mediaController.setTag(null);
|
|
||||||
Get.offAllNamed('/');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SizedBox.shrink();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
onChanged: (int? newValue) async {
|
onChanged: (int? newValue) async {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
await controller.setCrossAxisCount(newValue);
|
await controller.setCrossAxisCount(newValue);
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -93,7 +92,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
onChanged: (PageTransition? newValue) async {
|
onChanged: (PageTransition? newValue) async {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
await controller.setTransitionType(newValue);
|
await controller.setTransitionType(newValue);
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -42,6 +42,9 @@ class ApiService extends GetConnect {
|
|||||||
feed.items.sort((a, b) => b.id.compareTo(a.id));
|
feed.items.sort((a, b) => b.id.compareTo(a.id));
|
||||||
return feed;
|
return feed;
|
||||||
} else {
|
} else {
|
||||||
|
if (Get.isSnackbarOpen == false) {
|
||||||
|
Get.snackbar('Fehler', 'Fehler beim Laden der Items');
|
||||||
|
}
|
||||||
throw Exception('Fehler beim Laden der Items');
|
throw Exception('Fehler beim Laden der Items');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
|
|
||||||
class FavoriteAvatars extends StatelessWidget {
|
|
||||||
final List favorites;
|
|
||||||
final Brightness brightness;
|
|
||||||
|
|
||||||
const FavoriteAvatars({
|
|
||||||
super.key,
|
|
||||||
required this.favorites,
|
|
||||||
required this.brightness,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
...favorites.map((favorite) {
|
|
||||||
return Container(
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
margin: const EdgeInsets.only(right: 5.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: brightness == Brightness.dark
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black,
|
|
||||||
width: 1.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: favorite.avatarUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
74
lib/widgets/favoritesection.dart
Normal file
74
lib/widgets/favoritesection.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/controller/authcontroller.dart';
|
||||||
|
import 'package:f0ckapp/controller/mediacontroller.dart';
|
||||||
|
import 'package:f0ckapp/models/item.dart';
|
||||||
|
|
||||||
|
class FavoriteSection extends StatelessWidget {
|
||||||
|
final MediaItem item;
|
||||||
|
final int index;
|
||||||
|
final MediaController mediaController = Get.find<MediaController>();
|
||||||
|
final AuthController authController = Get.find<AuthController>();
|
||||||
|
|
||||||
|
FavoriteSection({super.key, required this.item, required this.index});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool isFavorite =
|
||||||
|
item.favorites?.any((f) => f.userId == authController.userId.value) ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
...(item.favorites ?? []).map((favorite) {
|
||||||
|
return Container(
|
||||||
|
height: 32,
|
||||||
|
width: 32,
|
||||||
|
margin: const EdgeInsets.only(right: 5.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).brightness == Brightness.dark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: favorite.avatarUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: isFavorite
|
||||||
|
? const Icon(Icons.favorite)
|
||||||
|
: const Icon(Icons.favorite_outline),
|
||||||
|
color: Colors.red,
|
||||||
|
onPressed: () async {
|
||||||
|
final List<Favorite>? newFavorites = await mediaController
|
||||||
|
.toggleFavorite(item, isFavorite);
|
||||||
|
if (newFavorites != null) {
|
||||||
|
mediaController.items[index] = item.copyWith(
|
||||||
|
favorites: newFavorites,
|
||||||
|
);
|
||||||
|
mediaController.items.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
import 'package:f0ckapp/models/item.dart';
|
import 'package:f0ckapp/models/item.dart';
|
||||||
|
|
||||||
@ -13,9 +12,6 @@ class MediaTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
|
||||||
Get.toNamed('/${item.id}');
|
|
||||||
},
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
|
30
lib/widgets/tagfooter.dart
Normal file
30
lib/widgets/tagfooter.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/controller/mediacontroller.dart';
|
||||||
|
|
||||||
|
class TagFooter extends StatelessWidget {
|
||||||
|
final MediaController mediaController = Get.find<MediaController>();
|
||||||
|
|
||||||
|
TagFooter({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Obx(() {
|
||||||
|
if (mediaController.tag.value != null) {
|
||||||
|
return Center(
|
||||||
|
child: InputChip(
|
||||||
|
label: Text(mediaController.tag.value!),
|
||||||
|
onDeleted: () {
|
||||||
|
mediaController.setTag(null);
|
||||||
|
Get.offAllNamed('/');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -14,12 +14,14 @@ class VideoWidget extends StatefulWidget {
|
|||||||
final MediaItem details;
|
final MediaItem details;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool fullScreen;
|
final bool fullScreen;
|
||||||
|
final VoidCallback? onInitialized;
|
||||||
|
|
||||||
const VideoWidget({
|
const VideoWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.details,
|
required this.details,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
this.fullScreen = false,
|
this.fullScreen = false,
|
||||||
|
this.onInitialized,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -29,6 +31,7 @@ class VideoWidget extends StatefulWidget {
|
|||||||
class _VideoWidgetState extends State<VideoWidget> {
|
class _VideoWidgetState extends State<VideoWidget> {
|
||||||
final MediaController controller = Get.find<MediaController>();
|
final MediaController controller = Get.find<MediaController>();
|
||||||
late CachedVideoPlayerPlusController _controller;
|
late CachedVideoPlayerPlusController _controller;
|
||||||
|
late Worker _muteWorker;
|
||||||
bool _showControls = false;
|
bool _showControls = false;
|
||||||
Timer? _hideControlsTimer;
|
Timer? _hideControlsTimer;
|
||||||
|
|
||||||
@ -36,6 +39,11 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initController();
|
_initController();
|
||||||
|
_muteWorker = ever(controller.muted, (bool muted) {
|
||||||
|
if (_controller.value.isInitialized) {
|
||||||
|
_controller.setVolume(muted ? 0.0 : 1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initController() async {
|
Future<void> _initController() async {
|
||||||
@ -43,6 +51,8 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||||||
Uri.parse(widget.details.mediaUrl),
|
Uri.parse(widget.details.mediaUrl),
|
||||||
);
|
);
|
||||||
await _controller.initialize();
|
await _controller.initialize();
|
||||||
|
widget.onInitialized?.call();
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
_controller.addListener(() => setState(() {}));
|
_controller.addListener(() => setState(() {}));
|
||||||
_controller.setLooping(true);
|
_controller.setLooping(true);
|
||||||
@ -67,6 +77,7 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_muteWorker.dispose();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_hideControlsTimer?.cancel();
|
_hideControlsTimer?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -87,11 +98,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final bool muted = controller.muted.value;
|
final bool muted = controller.muted.value;
|
||||||
if (_controller.value.isInitialized &&
|
|
||||||
_controller.value.volume != (muted ? 0.0 : 1.0)) {
|
|
||||||
_controller.setVolume(muted ? 0.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isAudio = widget.details.mime.startsWith('audio');
|
bool isAudio = widget.details.mime.startsWith('audio');
|
||||||
|
|
||||||
Widget mediaContent;
|
Widget mediaContent;
|
||||||
@ -131,7 +137,6 @@ class _VideoWidgetState extends State<VideoWidget> {
|
|||||||
muted: muted,
|
muted: muted,
|
||||||
onMuteToggle: () {
|
onMuteToggle: () {
|
||||||
controller.toggleMuted();
|
controller.toggleMuted();
|
||||||
setState(() {});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.4.0+61
|
version: 1.4.1+62
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0-100.2.beta
|
sdk: ^3.9.0-100.2.beta
|
||||||
|
Reference in New Issue
Block a user