Compare commits

...

4 Commits

Author SHA1 Message Date
dff9cda829 v1.1.23+53
All checks were successful
Flutter Schmutter / build (push) Successful in 3m40s
- bye go_router
2025-06-12 11:39:31 +02:00
16ebc51e77 v1.1.22+52
All checks were successful
Flutter Schmutter / build (push) Successful in 3m39s
2025-06-11 21:30:05 +02:00
7981436374 v1.1.21+51
All checks were successful
Flutter Schmutter / build (push) Successful in 3m40s
2025-06-11 20:45:37 +02:00
e38d2086b3 v1.1.20+50
All checks were successful
Flutter Schmutter / build (push) Successful in 3m39s
2025-06-11 18:56:55 +02:00
14 changed files with 657 additions and 467 deletions

View File

@ -28,19 +28,12 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="false"/>
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="f0ck.me"/> <data android:scheme="https" android:host="f0ck.me"/>
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="f0ck" android:host="com.f0ck.f0ckapp"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -1,14 +1,13 @@
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/screens/mediagrid_screen.dart'; import 'package:f0ckapp/screens/mediagrid_screen.dart';
import 'package:f0ckapp/screens/detailview_screen.dart'; import 'package:f0ckapp/screens/detailview_screen.dart';
import 'package:f0ckapp/screens/settings_screen.dart';
import 'package:f0ckapp/utils/appversion_util.dart'; import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart'; import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/providers/media_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -19,69 +18,61 @@ void main() async {
} }
class F0ckApp extends ConsumerWidget { class F0ckApp extends ConsumerWidget {
F0ckApp({super.key}); const F0ckApp({super.key});
final GoRouter _router = GoRouter( @override
initialLocation: '/', Widget build(BuildContext context, WidgetRef ref) {
routes: [ final ThemeData theme = ref.watch(themeNotifierProvider);
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MediaGrid();
},
),
GoRoute(
path: '/:rest(.*)',
builder: (context, state) {
final bool isInternalLink = (state.extra is bool && state.extra == true);
final String fullPath = state.matchedLocation;
final regExp = RegExp( return MaterialApp(
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$', debugShowCheckedModeBanner: false,
theme: theme,
initialRoute: '/',
routes: {
'/': (context) => const MediaGrid(),
'/settings': (context) => const SettingsPage(),
},
onGenerateRoute: (RouteSettings settings) {
final String? name = settings.name;
if (name == null) {
return MaterialPageRoute(
builder: (_) =>
const Scaffold(body: Center(child: Text('Ungültiger Link'))),
settings: settings,
); );
final RegExpMatch? match = regExp.firstMatch(fullPath); }
if (match == null) { final RegExp regExp = RegExp(
return const Scaffold(body: Center(child: Text('Ungültiger Link'))); r'^(?:/tag/(?<tag>[^/]+))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
} );
final RegExpMatch? match = regExp.firstMatch(name);
if (match != null) {
final String? tag = match.namedGroup('tag'); final String? tag = match.namedGroup('tag');
final String? mime = match.namedGroup('mime'); final String? mime = match.namedGroup('mime');
final String? idStr = match.namedGroup('itemid'); final String? idStr = match.namedGroup('itemid');
final int? itemId = idStr != null ? int.tryParse(idStr) : null; final int? itemId = idStr != null ? int.tryParse(idStr) : null;
const int preloadOffset = 50; const int preloadOffset = 50;
return Consumer( if (itemId != null) {
builder: (BuildContext context, WidgetRef ref, Widget? child) { return MaterialPageRoute(
WidgetsBinding.instance.addPostFrameCallback((_) async { builder: (context) => DetailView(initialItemId: itemId),
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier); settings: settings,
if (!isInternalLink) { );
mediaNotifier.setType(mime ?? "alles"); }
mediaNotifier.setTag(tag);
}
if (itemId != null) {
await mediaNotifier.loadMedia(id: itemId + preloadOffset);
}
});
if (itemId != null) {
return DetailView(initialItemId: itemId);
} else {
return MediaGrid();
}
},
);
},
),
],
);
@override return MaterialPageRoute(
Widget build(BuildContext context, WidgetRef ref) { builder: (context) => const MediaGrid(),
final ThemeData theme = ref.watch(themeNotifierProvider); settings: settings,
return MaterialApp.router( );
debugShowCheckedModeBanner: false, }
routerConfig: _router,
theme: theme, return MaterialPageRoute(
builder: (context) =>
const Scaffold(body: Center(child: Text('Ungültiger Link'))),
settings: settings,
);
},
); );
} }
} }

View File

@ -115,6 +115,17 @@ class MediaNotifier extends StateNotifier<MediaState> {
} }
} }
List<MediaItem> mergeMediaItems(
List<MediaItem> current,
List<MediaItem> incoming,
) {
final existingIds = current.map((item) => item.id).toSet();
final newItems = incoming
.where((item) => !existingIds.contains(item.id))
.toList();
return [...current, ...newItems];
}
Future<void> loadMedia({int? id}) async { Future<void> loadMedia({int? id}) async {
if (state.isLoading) return; if (state.isLoading) return;
state = state.replace(isLoading: true); state = state.replace(isLoading: true);
@ -128,8 +139,11 @@ class MediaNotifier extends StateNotifier<MediaState> {
random: state.random, random: state.random,
tag: state.tag, tag: state.tag,
); );
if (newMedia.isNotEmpty) { if (newMedia.isNotEmpty) {
addMediaItems(newMedia); state = state.replace(
mediaItems: mergeMediaItems(state.mediaItems, newMedia),
);
} }
} catch (e) { } catch (e) {
print('Fehler beim Laden der Medien: $e'); print('Fehler beim Laden der Medien: $e');

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -232,10 +233,11 @@ final ThemeData f0ck95Theme = ThemeData(
onPrimary: Colors.black, onPrimary: Colors.black,
onSecondary: Colors.white, onSecondary: Colors.white,
), ),
appBarTheme: const AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: Color(0xFFC0C0C0), backgroundColor: const Color(0xFFE0E0E0),
foregroundColor: Colors.black, foregroundColor: Colors.black,
elevation: 2, elevation: 4,
centerTitle: true
), ),
textTheme: const TextTheme( textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black), bodyLarge: TextStyle(color: Colors.black),

View File

@ -1,14 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:f0ckapp/screens/fullscreen_screen.dart'; import 'package:f0ckapp/screens/fullscreen_screen.dart';
import 'package:f0ckapp/widgets/end_drawer.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/mediaitem_model.dart'; import 'package:f0ckapp/models/mediaitem_model.dart';
@ -72,7 +71,9 @@ class _DetailViewState extends ConsumerState<DetailView> {
Future<void> _downloadMedia() async { Future<void> _downloadMedia() async {
final MediaState mediaState = ref.read(mediaProvider); final MediaState mediaState = ref.read(mediaProvider);
final MediaItem currentItem = mediaState.mediaItems[_currentIndex]; final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
final File file = await DefaultCacheManager().getSingleFile(currentItem.mediaUrl); final File file = await DefaultCacheManager().getSingleFile(
currentItem.mediaUrl,
);
final MethodChannel methodChannel = const MethodChannel('MediaShit'); final MethodChannel methodChannel = const MethodChannel('MediaShit');
bool? success = await methodChannel.invokeMethod<bool>('saveFile', { bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
@ -81,7 +82,9 @@ class _DetailViewState extends ConsumerState<DetailView> {
}); });
success == true success == true
? _showMsg('${currentItem.dest} wurde in Downloads/fApp neigespeichert.') ? _showMsg(
'${currentItem.dest} wurde in Downloads/fApp neigespeichert.',
)
: _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.'); : _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.');
} }
@ -100,9 +103,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
if (itemIndex == -1) { if (itemIndex == -1) {
Future.microtask(() { Future.microtask(() {
ref ref.read(mediaProvider.notifier).loadMedia(id: widget.initialItemId + 50);
.read(mediaProvider.notifier)
.loadMedia(id: widget.initialItemId + 50);
}); });
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
@ -120,102 +121,8 @@ class _DetailViewState extends ConsumerState<DetailView> {
} }
return Scaffold( return Scaffold(
appBar: AppBar( endDrawer: EndDrawer(ref: ref),
centerTitle: true, endDrawerEnableOpenDragGesture: false,
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go('/', extra: true);
},
),
actions: [
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
final mediaState = ref.read(mediaProvider);
final currentItem = mediaState.mediaItems[_currentIndex];
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => FullScreenMediaView(item: currentItem),
),
);
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: _downloadMedia,
),
PopupMenuButton<String>(
onSelected: (value) async {
final item = mediaState.mediaItems[_currentIndex];
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),
),
],
),
body: Stack(
children: [
PageTransformer(
controller: _pageController!,
pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea(
child: SmartRefreshIndicator(
onRefresh: _loadMoreMedia,
child: _buildMediaItem(item, _currentIndex == itemIndex),
),
);
}).toList(),
),
],
),
persistentFooterButtons: mediaState.tag != null persistentFooterButtons: mediaState.tag != null
? [ ? [
Center( Center(
@ -223,12 +130,124 @@ class _DetailViewState extends ConsumerState<DetailView> {
label: Text(mediaState.tag!), label: Text(mediaState.tag!),
onDeleted: () { onDeleted: () {
ref.read(mediaProvider.notifier).setTag(null); ref.read(mediaProvider.notifier).setTag(null);
context.go('/', extra: true); //context.push('/', extra: true);
Navigator.pushNamed(context, '/');
}, },
), ),
), ),
] ]
: null, : null,
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
snap: true,
centerTitle: true,
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushNamed(context, '/');
//context.canPop() ? context.pop() : context.go('/', extra: true);
},
),
actions: [
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
final currentItem = mediaState.mediaItems[_currentIndex];
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => FullScreenMediaView(item: currentItem),
),
);
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: _downloadMedia,
),
PopupMenuButton<String>(
onSelected: (value) async {
final item = mediaState.mediaItems[_currentIndex];
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();
},
),
),
],
),
SliverPadding(
padding: EdgeInsets.zero,
sliver: SliverFillRemaining(
child: PageTransformer(
controller: _pageController!,
pages: mediaState.mediaItems.map((item) {
int pageIndex = mediaState.mediaItems.indexOf(item);
return SafeArea(
top: false,
child: SmartRefreshIndicator(
onRefresh: _loadMoreMedia,
child: _buildMediaItem(item, _currentIndex == pageIndex),
),
);
}).toList(),
),
),
),
],
),
); );
} }
@ -242,8 +261,10 @@ class _DetailViewState extends ConsumerState<DetailView> {
CachedNetworkImage( CachedNetworkImage(
imageUrl: item.mediaUrl, imageUrl: item.mediaUrl,
fit: BoxFit.contain, fit: BoxFit.contain,
placeholder: (context, url) => const CircularProgressIndicator(), placeholder: (context, url) =>
errorWidget: (context, url, error) => const Icon(Icons.error), const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
const Center(child: Icon(Icons.error)),
) )
else else
VideoWidget(details: item, isActive: isActive), VideoWidget(details: item, isActive: isActive),
@ -257,7 +278,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return; if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
setState(() { setState(() {
mediaNotifier.setTag(tag.tag); mediaNotifier.setTag(tag.tag);
context.go('/', extra: true); Navigator.pushReplacementNamed(context, '/');
}); });
}, },
label: Text(tag.tag), label: Text(tag.tag),

View File

@ -40,7 +40,7 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
child: widget.item.mime.startsWith('image') child: widget.item.mime.startsWith('image')
? InteractiveViewer( ? InteractiveViewer(
minScale: 1.0, minScale: 1.0,
maxScale: 6.0, maxScale: 7.0,
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: widget.item.mediaUrl, imageUrl: widget.item.mediaUrl,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -62,7 +62,7 @@ class _FullScreenMediaViewState extends State<FullScreenMediaView> {
child: Align( child: Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: IconButton( child: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
), ),

View File

@ -1,14 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/providers/media_provider.dart'; import 'package:f0ckapp/providers/media_provider.dart';
import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/utils/customsearchdelegate_util.dart'; import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
import 'package:f0ckapp/widgets/media_tile.dart';
import 'package:f0ckapp/widgets/filter_bar.dart';
import 'package:f0ckapp/widgets/end_drawer.dart';
class MediaGrid extends ConsumerStatefulWidget { class MediaGrid extends ConsumerStatefulWidget {
const MediaGrid({super.key}); const MediaGrid({super.key});
@ -19,16 +17,6 @@ class MediaGrid extends ConsumerStatefulWidget {
class _MediaGridState extends ConsumerState<MediaGrid> { class _MediaGridState extends ConsumerState<MediaGrid> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
int _calculateCrossAxisCount(BuildContext context, int defaultCount) {
return defaultCount == 0
? (MediaQuery.of(context).size.width / 110).clamp(3, 5).toInt()
: defaultCount;
}
@override @override
void initState() { void initState() {
@ -47,8 +35,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
@override @override
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose(); super.dispose();
} }
@ -58,197 +44,106 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier); final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold( return Scaffold(
key: _scaffoldKey, body: RefreshIndicator(
appBar: AppBar( onRefresh: () async {
title: GestureDetector( mediaNotifier.setTag(null);
child: Row( _scrollController.jumpTo(0);
spacing: 10, await mediaNotifier.loadMedia();
children: [ },
Image.asset( child: CustomScrollView(
'assets/images/f0ck_small.webp', controller: _scrollController,
fit: BoxFit.fitHeight, slivers: [
SliverAppBar(
floating: true,
snap: true,
title: GestureDetector(
onTap: () {
mediaNotifier.setTag(null);
_scrollController.jumpTo(0);
},
child: Row(
children: [
Image.asset(
'assets/images/f0ck_small.webp',
fit: BoxFit.fitHeight,
),
const SizedBox(width: 10),
const Text('fApp', style: TextStyle(fontSize: 24)),
],
),
), ),
Text('fApp', style: TextStyle(fontSize: 24)), actions: [
], IconButton(
), icon: const Icon(Icons.search),
onTap: () { onPressed: () async {
mediaNotifier.setTag(null); await showSearch(
_scrollController.jumpTo(0); context: context,
}, delegate: CustomSearchDelegate(),
), );
actions: [ },
IconButton( ),
icon: const Icon(Icons.search), IconButton(
onPressed: () async { icon: Icon(
await showSearch( mediaState.random
context: context, ? Icons.shuffle_on_outlined
delegate: CustomSearchDelegate(), : Icons.shuffle,
); ),
}, onPressed: () {
), mediaNotifier.toggleRandom();
IconButton( _scrollController.jumpTo(0);
icon: Icon( },
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle, ),
Builder(
builder: (context) {
return IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
);
},
),
],
), ),
onPressed: () { SliverPadding(
mediaNotifier.toggleRandom(); padding: EdgeInsets.zero,
_scrollController.jumpTo(0); sliver: SliverGrid(
}, delegate: SliverChildBuilderDelegate(
), (context, index) {
IconButton( if (index >= mediaState.mediaItems.length) {
icon: const Icon(Icons.menu), return const Center(child: CircularProgressIndicator());
onPressed: () { }
_scaffoldKey.currentState?.openEndDrawer(); return MediaTile(item: mediaState.mediaItems[index]);
}, },
), childCount:
], mediaState.mediaItems.length +
), (mediaState.isLoading ? 1 : 0),
bottomNavigationBar: BottomAppBar( ),
height: 50, gridDelegate: mediaState.crossAxisCount == 0
child: Row( ? const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisAlignment: MainAxisAlignment.spaceAround, maxCrossAxisExtent: 150,
children: [ crossAxisSpacing: 5,
const Text('type: '), mainAxisSpacing: 5,
DropdownButton<String>( childAspectRatio: 1,
value: mediaTypes[mediaState.typeIndex], )
isDense: true, : SliverGridDelegateWithFixedCrossAxisCount(
items: mediaTypes.map((String value) { crossAxisCount: mediaState.crossAxisCount,
return DropdownMenuItem<String>( crossAxisSpacing: 5,
value: value, mainAxisSpacing: 5,
child: Text(value), childAspectRatio: 1,
); ),
}).toList(), ),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setType(newValue);
_scrollController.jumpTo(0);
}
},
),
const Text('mode: '),
DropdownButton<String>(
value: mediaModes[mediaState.modeIndex],
isDense: true,
items: mediaModes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setMode(mediaModes.indexOf(newValue));
_scrollController.jumpTo(0);
}
},
), ),
], ],
), ),
), ),
endDrawer: Drawer( bottomNavigationBar: FilterBar(
child: ListView( mediaNotifier: mediaNotifier,
padding: EdgeInsets.zero, mediaState: mediaState,
children: [ scrollController: _scrollController,
DrawerHeader(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/menu.webp'),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
child: null,
),
/*ExpansionTile(
title: const Text('Login'),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
readOnly: true,
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Benutzername',
),
),
const SizedBox(height: 10),
TextField(
readOnly: true,
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Passwort',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("noch nicht implementiert lol"),
),
final success = await login(
_usernameController.text,
_passwordController.text,
);
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login fehlgeschlagen!")),
);
}
);
},
child: const Text('Login'),
),
],
),
),
],
),*/
ExpansionTile(
title: const Text('Theme'),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: themeMap.entries.map((entry) {
final String themeName = entry.key;
final ThemeData themeData = entry.value;
final ThemeData currentTheme = ref.watch(themeNotifierProvider);
final bool isSelected = currentTheme == themeData;
return ListTile(
title: Text(themeName),
selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.2),
onTap: () async {
await ref
.read(themeNotifierProvider.notifier)
.updateTheme(themeName);
},
);
}).toList(),
),
),
],
),
ListTile(
title: Text('v${AppVersion.version}'),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('jooong lass das, hier ist nichts'),
),
);
},
),
],
),
), ),
endDrawer: EndDrawer(ref: ref),
endDrawerEnableOpenDragGesture: false,
persistentFooterButtons: mediaState.tag != null persistentFooterButtons: mediaState.tag != null
? [ ? [
Center( Center(
@ -262,62 +157,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
] ]
: null, : null,
body: RefreshIndicator(
onRefresh: () async {
mediaNotifier.resetMedia();
_scrollController.jumpTo(0);
},
child: GridView.builder(
key: const PageStorageKey('mediaGrid'),
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _calculateCrossAxisCount(
context,
mediaState.crossAxisCount,
),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount:
mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0),
itemBuilder: (BuildContext context, int index) {
if (index >= mediaState.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final MediaItem item = mediaState.mediaItems[index];
return InkWell(
onTap: () async {
context.push('/${item.id}', extra: true);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
CachedNetworkImage(
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => const SizedBox.shrink(),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
Align(
alignment: Alignment.bottomRight,
child: Icon(
Icons.square,
color: switch (item.mode) {
1 => Colors.green,
2 => Colors.red,
_ => Colors.yellow,
},
size: 15.0,
),
),
],
),
);
},
),
),
); );
} }
} }

View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:f0ckapp/widgets/end_drawer.dart';
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key});
@override
ConsumerState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
int _columns = 3;
bool _drawerSwipeEnabled = true;
@override
Widget build(BuildContext context) {
final MediaState mediaState = ref.watch(mediaProvider);
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold(
endDrawerEnableOpenDragGesture: _drawerSwipeEnabled,
endDrawer: EndDrawer(ref: ref),
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: false,
pinned: true,
title: const Text('Settings'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushReplacementNamed(context, '/');
},
),
),
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Anzahl der Spalten",
style: Theme.of(context).textTheme.titleLarge,
),
),
ListTile(
title: const Text("Spaltenanzahl"),
trailing: DropdownButton<int>(
value: mediaState.crossAxisCount,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: [0, 3, 4, 5].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(value == 0 ? 'auto' : '$value Spalten'),
);
}).toList(),
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_columns = newValue;
mediaNotifier.setCrossAxisCount(newValue);
});
}
},
),
),
const Divider(),
SwitchListTile(
title: const Text("Drawer per Geste öffnen"),
subtitle: const Text(
"Wähle, ob der Drawer mit einer Wischgeste geschlossen/ geöffnet werden kann.",
),
value: _drawerSwipeEnabled,
onChanged: (bool value) {
setState(() {
_drawerSwipeEnabled = value;
});
},
),
const Divider(),
ListTile(
title: const Text("Cache löschen"),
trailing: ElevatedButton(
onPressed: () async {
await DefaultCacheManager().emptyCache();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Cache wurde geleert.")),
);
},
child: const Text("Löschen"),
),
),
const SizedBox(height: 20),
]),
),
],
),
);
}
}

View File

@ -13,16 +13,16 @@ final FlutterSecureStorage storage = const FlutterSecureStorage(
Future<List<MediaItem>> fetchMedia({ Future<List<MediaItem>> fetchMedia({
int? older, int? older,
String? type, String type = 'image',
int? mode, int mode = 0,
bool? random, bool random = false,
String? tag, String? tag,
}) async { }) async {
final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace( final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace(
queryParameters: { queryParameters: {
'type': type ?? 'image', 'type': type,
'mode': (mode ?? 0).toString(), 'mode': mode.toString(),
'random': (random! ? 1 : 0).toString(), 'random': (random ? 1 : 0).toString(),
if (tag != null) 'tag': tag, if (tag != null) 'tag': tag,
if (older != null) 'older': older.toString(), if (older != null) 'older': older.toString(),
}, },
@ -61,14 +61,14 @@ Future<List<Suggestion>> fetchSuggestions(String query) async {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final dynamic decoded = jsonDecode(response.body); final dynamic decoded = jsonDecode(response.body);
if (decoded is List) { if (decoded is List) {
return decoded final suggestions = decoded
.map((item) => Suggestion.fromJson(item as Map<String, dynamic>)) .map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
.toList() .toList();
..sort((a, b) => b.score.compareTo(a.score)); suggestions.sort((a, b) => b.score.compareTo(a.score));
return suggestions;
} else { } else {
throw Exception('Unerwartetes Format: Erwartet wurde eine Liste.'); throw Exception('Unerwartetes Format: Es wurde eine Liste erwartet.');
} }
} else if (response.statusCode == 400) { } else if (response.statusCode == 400) {
final dynamic error = jsonDecode(response.body); final dynamic error = jsonDecode(response.body);
@ -84,7 +84,7 @@ Future<List<Suggestion>> fetchSuggestions(String query) async {
} on TimeoutException { } on TimeoutException {
throw Exception('Anfrage an die API hat zu lange gedauert.'); throw Exception('Anfrage an die API hat zu lange gedauert.');
} catch (e) { } catch (e) {
throw Exception('Fehler beim Verarbeiten der Anfrage: $e'); throw Exception('Fehler bei der Verarbeitung der Anfrage: $e');
} }
} }
@ -98,12 +98,14 @@ Future<bool> login(String username, String password) async {
if (response.statusCode == 200) { if (response.statusCode == 200) {
final dynamic data = jsonDecode(response.body); final dynamic data = jsonDecode(response.body);
final dynamic token = data['token']; final token = data['token'];
if (token != null) {
await storage.write(key: "token", value: token); await storage.write(key: "token", value: token);
return true;
return true; } else {
throw Exception('Token nicht im Response enthalten.');
}
} else { } else {
return false; throw Exception('Login fehlgeschlagen: ${response.statusCode}');
} }
} }

132
lib/widgets/end_drawer.dart Normal file
View File

@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/utils/appversion_util.dart';
class EndDrawer extends StatelessWidget {
final WidgetRef ref;
const EndDrawer({super.key, required this.ref});
void _showMsg(String message, BuildContext context) {
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/menu.webp'),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
child: null,
),
/*ExpansionTile(
title: const Text('Login'),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
readOnly: true,
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Benutzername',
),
),
const SizedBox(height: 10),
TextField(
readOnly: true,
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Passwort',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("noch nicht implementiert lol"),
),
final success = await login(
_usernameController.text,
_passwordController.text,
);
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login fehlgeschlagen!")),
);
}
);
},
child: const Text('Login'),
),
],
),
),
],
),*/
ExpansionTile(
title: const Text('Theme'),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: themeMap.entries.map((entry) {
final String themeName = entry.key;
final ThemeData themeData = entry.value;
final ThemeData currentTheme = ref.watch(
themeNotifierProvider,
);
final bool isSelected = currentTheme == themeData;
return ListTile(
title: Text(themeName),
selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.2),
onTap: () async {
await ref
.read(themeNotifierProvider.notifier)
.updateTheme(themeName);
},
);
}).toList(),
),
),
],
),
ListTile(
title: const Text('Einstellungen'),
onTap: () {
//context.go('/settings');
Navigator.pushReplacementNamed(context, '/settings');
},
),
ListTile(
title: Text('v${AppVersion.version}'),
onTap: () {
Navigator.pop(context);
_showMsg('jooong lass das, hier ist nichts', context);
},
),
],
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:f0ckapp/providers/media_provider.dart';
class FilterBar extends StatelessWidget {
final MediaState mediaState;
final MediaNotifier mediaNotifier;
final ScrollController scrollController;
const FilterBar({
super.key,
required this.mediaState,
required this.mediaNotifier,
required this.scrollController,
});
@override
Widget build(BuildContext context) {
return BottomAppBar(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Text('type: '),
DropdownButton<String>(
value: mediaTypes[mediaState.typeIndex],
isDense: true,
items: mediaTypes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setType(newValue);
scrollController.jumpTo(0);
}
},
),
const Text('mode: '),
DropdownButton<String>(
value: mediaModes[mediaState.modeIndex],
isDense: true,
items: mediaModes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setMode(mediaModes.indexOf(newValue));
scrollController.jumpTo(0);
}
},
),
],
),
);
}
}

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
class MediaTile extends StatelessWidget {
final MediaItem item;
const MediaTile({super.key, required this.item});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Navigator.pushNamed(context, '/${item.id}');
},
child: Stack(
fit: StackFit.expand,
children: [
Hero(
tag: 'media-${item.id}',
child: CachedNetworkImage(
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
errorWidget: (context, url, error) => const Icon(Icons.error),
),
),
Align(
alignment: Alignment.bottomRight,
child: Icon(
Icons.square,
color: switch (item.mode) {
1 => Colors.green,
2 => Colors.red,
_ => Colors.yellow,
},
size: 15.0,
),
),
],
),
);
}
}

View File

@ -240,14 +240,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175
url: "https://pub.dev"
source: hosted
version: "15.1.3"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -312,14 +304,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -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.1.19+49 version: 1.1.23+53
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta
@ -41,7 +41,6 @@ dependencies:
share_plus: ^11.0.0 share_plus: ^11.0.0
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
go_router: ^15.1.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: