diff --git a/lib/main.dart b/lib/main.dart index 2e23b83..9e91f64 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), ), ); diff --git a/lib/mediagrid.dart b/lib/mediagrid.dart index af3d124..70c10d5 100644 --- a/lib/mediagrid.dart +++ b/lib/mediagrid.dart @@ -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 { final ScrollController _scrollController = ScrollController(); List mediaItems = []; bool isLoading = false; + Timer? _debounceTimer; + Completer? _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 _loadMedia() async { if (isLoading) return; setState(() => isLoading = true); try { - List 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 _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 _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( + 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( + 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: [ + 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(); + } } diff --git a/lib/models/mediaitem_detail.dart b/lib/models/mediaitem_detail.dart index 4339d12..d12334c 100644 --- a/lib/models/mediaitem_detail.dart +++ b/lib/models/mediaitem_detail.dart @@ -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, diff --git a/lib/screens/detail copy.dart b/lib/screens/detail copy.dart deleted file mode 100644 index 3f10366..0000000 --- a/lib/screens/detail copy.dart +++ /dev/null @@ -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'); - } - } -} diff --git a/lib/screens/detail.dart b/lib/screens/detail.dart index 562b233..fb53ff6 100644 --- a/lib/screens/detail.dart +++ b/lib/screens/detail.dart @@ -84,10 +84,10 @@ class _DetailViewState extends State { 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( diff --git a/lib/services/api.dart b/lib/services/api.dart index c3d6ec3..3ed4469 100644 --- a/lib/services/api.dart +++ b/lib/services/api.dart @@ -21,7 +21,7 @@ Future> fetchMedia({String? older}) async { } Future 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) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..f6f23bf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..f16b4c3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 68402fa..7a32065 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,20 @@ import FlutterMacOS import Foundation +import device_info_plus +import package_info_plus import path_provider_foundation +import shared_preferences_foundation import sqflite_darwin +import url_launcher_macos import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 462ff52..9d9aace 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + device_info_plus: + dependency: transitive + description: + name: device_info_plus + sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + url: "https://pub.dev" + source: hosted + version: "7.0.2" fake_async: dependency: transitive description: @@ -240,6 +256,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + os_detect: + dependency: transitive + description: + name: os_detect + sha256: "7d87c0dd98c6faf110d5aa498e9a6df02ffce4bb78cc9cfc8ad02929be9bb71f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" + url: "https://pub.dev" + source: hosted + version: "8.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" + url: "https://pub.dev" + source: hosted + version: "3.2.0" path: dependency: transitive description: @@ -296,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" platform: dependency: transitive description: @@ -320,6 +368,62 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -437,6 +541,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + upgrader: + dependency: "direct main" + description: + name: upgrader + sha256: "60293b391f4146ce1e381ed6198b6374559a4aadf885d313e94e6c7d454d03ca" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: @@ -453,6 +629,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + version: + dependency: transitive + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" video_player: dependency: "direct main" description: @@ -509,6 +693,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" xdg_directories: dependency: transitive description: @@ -517,6 +717,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" sdks: dart: ">=3.9.0-172.0.dev <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7e8a249..493d270 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + upgrader: ^11.4.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..4f78848 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,9 @@ #include "generated_plugin_registrant.h" +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..88b22e5 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST