Compare commits

...

5 Commits

Author SHA1 Message Date
671b3cfbe0 v1.1.9+39
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
2025-06-09 14:02:59 +02:00
93fb3536ee v1.1.8+38
All checks were successful
Flutter Schmutter / build (push) Successful in 3m48s
- blah
2025-06-08 19:40:06 +02:00
346e447d5e v1.1.7+37
All checks were successful
Flutter Schmutter / build (push) Successful in 3m44s
- worst update eu west
2025-06-08 17:16:10 +02:00
f7777821fd iiiiiicon 2025-06-08 10:33:05 +02:00
ffbde73300 v1.1.6+36
All checks were successful
Flutter Schmutter / build (push) Successful in 3m34s
- new theme: p1nk
- optimizations
2025-06-07 23:07:31 +02:00
47 changed files with 355 additions and 133 deletions

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:label="f0ckapp" android:label="f0ckapp"
@ -28,12 +29,26 @@
<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="true"/>
<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="https" android:host="f0ck.me"/>
</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 -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,33 +1,87 @@
import 'package:f0ckapp/providers/MediaProvider.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:f0ckapp/screens/MediaGrid.dart'; import 'package:f0ckapp/screens/MediaGrid.dart';
import 'package:f0ckapp/screens/DetailView.dart';
import 'package:f0ckapp/utils/AppVersion.dart'; import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart'; import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppVersion.init(); await AppVersion.init();
runApp(ProviderScope(child: const F0ckApp())); runApp(ProviderScope(child: F0ckApp()));
} }
class F0ckApp extends ConsumerWidget { class F0ckApp extends ConsumerWidget {
const F0ckApp({super.key}); F0ckApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) {
return const MediaGrid();
},
),
GoRoute(
path: '/:rest(.*)',
builder: (context, state) {
final isInternalLink = (state.extra is bool && state.extra == true);
final fullPath = state.matchedLocation;
final regExp = RegExp(
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
);
final match = regExp.firstMatch(fullPath);
if (match == null) {
return const Scaffold(body: Center(child: Text('Ungültiger Link')));
}
final String? tag = match.namedGroup('tag');
final String? mime = match.namedGroup('mime');
final String? idStr = match.namedGroup('itemid');
final int? itemId = idStr != null ? int.tryParse(idStr) : null;
const preloadOffset = 50;
return Consumer(
builder: (context, ref, child) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final mediaNotifier = ref.read(mediaProvider.notifier);
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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Consumer( final theme = ref.watch(themeNotifierProvider);
builder: (context, ref, _) { return MaterialApp.router(
return MaterialApp( debugShowCheckedModeBanner: false,
debugShowCheckedModeBanner: false, routerConfig: _router,
theme: ref.watch(themeNotifierProvider), theme: theme,
home: const MediaGrid(),
);
},
); );
} }
} }

View File

@ -106,14 +106,12 @@ class MediaNotifier extends StateNotifier<MediaState> {
state = state.replace(mediaItems: updated); state = state.replace(mediaItems: updated);
} }
Future<void> loadMedia() 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);
try { try {
final older = state.mediaItems.isNotEmpty final older =
? state.mediaItems.last.id id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
: null;
final newMedia = await fetchMedia( final newMedia = await fetchMedia(
older: older, older: older,
type: mediaTypes[state.typeIndex], type: mediaTypes[state.typeIndex],

View File

@ -1,8 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
final Map<String, ThemeData> themeMap = {
'f0ck': f0ckTheme,
'P1nk': p1nkTheme, // done
'Orange': orangeTheme,
'Amoled': amoledTheme,
'Paper': paperTheme,
//'Iced': icedTheme,
'f0ck95': f0ck95Theme,
'f0ck95d': f0ck95dTheme,
};
class ThemeNotifier extends StateNotifier<ThemeData> { class ThemeNotifier extends StateNotifier<ThemeData> {
final FlutterSecureStorage secureStorage; final FlutterSecureStorage secureStorage;
@ -11,23 +21,24 @@ class ThemeNotifier extends StateNotifier<ThemeData> {
} }
Future<void> _loadTheme() async { Future<void> _loadTheme() async {
String? savedThemeName = await secureStorage.read(key: 'theme'); try {
if (savedThemeName != null) { String? savedThemeName = await secureStorage.read(key: 'theme');
final customTheme = themes.firstWhere( if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
(t) => t.name == savedThemeName, state = themeMap[savedThemeName]!;
orElse: () => CustomTheme(name: 'f0ck', theme: f0ckTheme), }
); } catch (error) {
state = customTheme.theme; debugPrint('Fehler beim Laden des Themes: $error');
state = f0ckTheme;
} }
} }
Future<void> updateTheme(String themeName) async { Future<void> updateTheme(String themeName) async {
await secureStorage.write(key: 'theme', value: themeName); try {
final newTheme = themes.firstWhere( await secureStorage.write(key: 'theme', value: themeName);
(t) => t.name == themeName, state = themeMap[themeName] ?? f0ckTheme;
orElse: () => CustomTheme(name: 'f0ck', theme: f0ckTheme), } catch (error) {
); debugPrint('Fehler beim Aktualisieren des Themes: $error');
state = newTheme.theme; }
} }
} }
@ -35,28 +46,12 @@ final themeNotifierProvider = StateNotifierProvider<ThemeNotifier, ThemeData>((
ref, ref,
) { ) {
return ThemeNotifier( return ThemeNotifier(
secureStorage: FlutterSecureStorage( secureStorage: const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true), aOptions: AndroidOptions(encryptedSharedPreferences: true),
), ),
); );
}); });
class CustomTheme {
final String name;
final ThemeData theme;
CustomTheme({required this.name, required this.theme});
}
final List<CustomTheme> themes = [
CustomTheme(name: 'f0ck', theme: f0ckTheme),
CustomTheme(name: 'Paper', theme: paperTheme),
CustomTheme(name: 'Orange', theme: orangeTheme),
CustomTheme(name: 'Amoled', theme: amoledTheme),
CustomTheme(name: 'f0ck95', theme: f0ck95Theme),
CustomTheme(name: 'f0ck95d', theme: f0ck95dTheme),
];
final ThemeData f0ckTheme = ThemeData( final ThemeData f0ckTheme = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
primaryColor: const Color(0xFF9FFF00), primaryColor: const Color(0xFF9FFF00),
@ -78,13 +73,53 @@ final ThemeData f0ckTheme = ThemeData(
bodyLarge: TextStyle(color: Colors.white), bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white), bodyMedium: TextStyle(color: Colors.white),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: const Color(0xFF9FFF00), style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: const Color(0xFF000000),
backgroundColor: const Color(0xFF9FFF00),
),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)), thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)), trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData p1nkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF171717),
scaffoldBackgroundColor: const Color(0xFF171717),
appBarTheme: const AppBarTheme(
color: Color(0xFF2B2B2B),
foregroundColor: Color(0xFFFF00D0),
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Color(0xFFFFFFFF)),
bodyMedium: TextStyle(color: Color(0xFFFFFFFF)),
titleLarge: TextStyle(color: Color(0xFFFFFFFF)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF000000),
backgroundColor: const Color(0xFFFF00D0),
),
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: Color(0xFFFF00D0),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all(const Color(0xFF424242)),
),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF171717),
secondary: Color(0xFFFF00D0),
surface: Color(0xFF171717),
onPrimary: Color(0xFFFFFFFF),
onSecondary: Color(0xFF000000),
onSurface: Color(0xFFFFFFFF),
error: Color(0xFFA72828),
), ),
); );
@ -109,9 +144,11 @@ final ThemeData paperTheme = ThemeData(
bodyLarge: TextStyle(color: Color(0xFF000000)), bodyLarge: TextStyle(color: Color(0xFF000000)),
bodyMedium: TextStyle(color: Color(0xFF000000)), bodyMedium: TextStyle(color: Color(0xFF000000)),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: const Color(0xFF000000), style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: const Color(0xFFFFFFFF),
backgroundColor: const Color(0xFF000000),
),
), ),
); );
@ -136,13 +173,15 @@ final ThemeData orangeTheme = ThemeData(
bodyLarge: TextStyle(color: Colors.white), bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white), bodyMedium: TextStyle(color: Colors.white),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: const Color(0xFFFF6F00), style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: Colors.white,
backgroundColor: const Color(0xFFFF6F00),
),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)), thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)), trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
), ),
); );
@ -169,9 +208,11 @@ final ThemeData amoledTheme = ThemeData(
bodyLarge: TextStyle(color: Colors.white), bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white), bodyMedium: TextStyle(color: Colors.white),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: const Color(0xFFFFFFFF), style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: Colors.black,
backgroundColor: const Color(0xFFFFFFFF),
),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF1D1C1C)), thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF1D1C1C)),
@ -199,13 +240,15 @@ final ThemeData f0ck95Theme = ThemeData(
bodyLarge: TextStyle(color: Colors.black), bodyLarge: TextStyle(color: Colors.black),
bodyMedium: TextStyle(color: Colors.black), bodyMedium: TextStyle(color: Colors.black),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: Colors.black, style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: Colors.white,
backgroundColor: Colors.black,
),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)), thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)), trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
), ),
); );
@ -229,12 +272,14 @@ final ThemeData f0ck95dTheme = ThemeData(
bodyLarge: TextStyle(color: Colors.white), bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white), bodyMedium: TextStyle(color: Colors.white),
), ),
buttonTheme: ButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
buttonColor: Colors.white, style: ElevatedButton.styleFrom(
textTheme: ButtonTextTheme.primary, foregroundColor: Colors.black,
backgroundColor: Colors.white,
),
), ),
scrollbarTheme: ScrollbarThemeData( scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)), thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)), trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
), ),
); );

View File

@ -2,10 +2,10 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
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: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:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/MediaItem.dart'; import 'package:f0ckapp/models/MediaItem.dart';
@ -24,40 +24,24 @@ class DetailView extends ConsumerStatefulWidget {
} }
class _DetailViewState extends ConsumerState<DetailView> { class _DetailViewState extends ConsumerState<DetailView> {
late PageController _pageController; PageController? _pageController;
bool isLoading = false; bool isLoading = false;
int _currentIndex = 0; int _currentIndex = 0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final mediaState = ref.read(mediaProvider);
final initialIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
_pageController = PageController(initialPage: initialIndex);
_currentIndex = initialIndex;
_pageController.addListener(() {
setState(() => _currentIndex = _pageController.page?.round() ?? 0);
});
_preloadAdjacentMedia(initialIndex);
} }
void _preloadAdjacentMedia(int index) async { void _preloadAdjacentMedia(int index) async {
final mediaState = ref.read(mediaProvider); final mediaState = ref.read(mediaProvider);
if (index + 1 < mediaState.mediaItems.length) { for (int offset in [-1, 1]) {
final nextUrl = mediaState.mediaItems[index + 1].mediaUrl; final adjacentIndex = index + offset;
if (await DefaultCacheManager().getFileFromCache(nextUrl) == null) { if (adjacentIndex >= 0 && adjacentIndex < mediaState.mediaItems.length) {
await DefaultCacheManager().downloadFile(nextUrl); final url = mediaState.mediaItems[adjacentIndex].mediaUrl;
} if (await DefaultCacheManager().getFileFromCache(url) == null) {
} await DefaultCacheManager().downloadFile(url);
if (index - 1 >= 0) { }
final prevUrl = mediaState.mediaItems[index - 1].mediaUrl;
if (await DefaultCacheManager().getFileFromCache(prevUrl) == null) {
await DefaultCacheManager().downloadFile(prevUrl);
} }
} }
} }
@ -84,31 +68,60 @@ class _DetailViewState extends ConsumerState<DetailView> {
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController?.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider); final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier); final int itemIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
if (mediaState.mediaItems.isEmpty) { if (itemIndex == -1) {
Future.microtask(() {
ref
.read(mediaProvider.notifier)
.loadMedia(id: widget.initialItemId + 50);
});
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: const Center(child: CircularProgressIndicator()), body: const Center(child: CircularProgressIndicator()),
); );
} }
if (_pageController == null) {
_pageController = PageController(initialPage: itemIndex);
_currentIndex = itemIndex;
_pageController!.addListener(() {
setState(() => _currentIndex = _pageController!.page?.round() ?? 0);
});
_preloadAdjacentMedia(itemIndex);
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'), 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: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.download), icon: const Icon(Icons.fullscreen),
onPressed: () { onPressed: () {
// wip _showError('fullscreen ist wip');
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: () {
_showError('download ist wip');
}, },
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
@ -167,7 +180,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
body: Stack( body: Stack(
children: [ children: [
PageTransformer( PageTransformer(
controller: _pageController, controller: _pageController!,
pages: mediaState.mediaItems.map((item) { pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item); int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea( return SafeArea(
@ -186,8 +199,8 @@ class _DetailViewState extends ConsumerState<DetailView> {
child: InputChip( child: InputChip(
label: Text(mediaState.tag!), label: Text(mediaState.tag!),
onDeleted: () { onDeleted: () {
mediaNotifier.setTag(null); ref.read(mediaProvider.notifier).setTag(null);
Navigator.pop(context); context.go('/', extra: true);
}, },
), ),
), ),
@ -221,7 +234,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);
Navigator.pop(context, true); context.go('/', extra: true);
}); });
}, },
label: Text(tag.tag), label: Text(tag.tag),

View File

@ -7,6 +7,7 @@ import 'package:f0ckapp/screens/DetailView.dart';
import 'package:f0ckapp/providers/MediaProvider.dart'; import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/utils/AppVersion.dart'; import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart'; import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:go_router/go_router.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"]; const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"]; const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
@ -208,17 +209,19 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
children: themes.map((themeData) { children: themeMap.entries.map((entry) {
final themeName = entry.key;
final themeData = entry.value;
final currentTheme = ref.watch(themeNotifierProvider); final currentTheme = ref.watch(themeNotifierProvider);
final isSelected = currentTheme == themeData.theme; final isSelected = currentTheme == themeData;
return ListTile( return ListTile(
title: Text(themeData.name), title: Text(themeName),
selected: isSelected, selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.2), selectedTileColor: Colors.blue.withValues(alpha: 0.2),
onTap: () async { onTap: () async {
await ref await ref
.read(themeNotifierProvider.notifier) .read(themeNotifierProvider.notifier)
.updateTheme(themeData.name); .updateTheme(themeName);
}, },
); );
}).toList(), }).toList(),
@ -226,7 +229,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
), ),
], ],
), ),
ListTile( ListTile(
title: Text('v${AppVersion.version}'), title: Text('v${AppVersion.version}'),
onTap: () { onTap: () {
@ -279,15 +281,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
return InkWell( return InkWell(
onTap: () async { onTap: () async {
bool? ret = await Navigator.push( context.push('/${item.id}', extra: true);
context,
MaterialPageRoute(
builder: (context) => DetailView(initialItemId: item.id),
),
);
if (ret != null && ret) {
_scrollController.jumpTo(0);
}
}, },
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/screens/DetailView.dart';
Map<String, RegExp> routes = {
//'login': RegExp(r'^/login/?$'),
//'user': RegExp(r'^/user/(?<user>.*)$'),
'complex': RegExp(
r'^/?'
r'(?:tag/(?<tag>.+?))?'
r'(?:/user/(?<username>.+?)/(?<set>f0cks|favs))?'
r'(?:/(?<media>image|audio|video))?'
r'(?:/(?<id>\d+))?'
r'$',
),
//'random': RegExp(r'^/random$'),
//'search': RegExp(r'^/search/?$'),
};
Map<String, dynamic> parseDeepLink(Uri? uri) {
if (uri == null) {
return {};
}
String url = uri.toString().replaceAll("https://f0ck.me", "");
for (final MapEntry<String, RegExp> entry in routes.entries) {
final String routeName = entry.key;
final RegExp pattern = entry.value;
final RegExpMatch? match = pattern.firstMatch(url.toString());
if (match != null) {
Map<String, String> params = <String, String>{};
for (String name in match.groupNames) {
params[name] = match.namedGroup(name) ?? '';
}
return {'route': routeName, 'params': params};
}
}
return {};
}
Future<void> handleComplexDeepLink(
Map<String, String> params,
BuildContext context,
WidgetRef ref,
ScrollController scrollController,
) async {
final media = params['media'];
const validMediaTypes = {'audio', 'video', 'image'};
if (media != null && validMediaTypes.contains(media)) {
ref.read(mediaProvider.notifier).setType(media);
} else {
ref.read(mediaProvider.notifier).setType('alles');
}
ref.read(mediaProvider.notifier).setMode(0); // wip
ref.read(mediaProvider.notifier).setTag(null);
final idParam = params['id'];
if (idParam == null || idParam.isEmpty) return;
final int? id = int.tryParse(idParam);
if (id == null) return;
final mediaState = ref.read(mediaProvider);
final index = mediaState.mediaItems.indexWhere((item) => item.id == id);
if (index == -1) {
await ref.read(mediaProvider.notifier).loadMedia(id: id + 50);
final updatedState = ref.read(mediaProvider);
final updatedIndex = updatedState.mediaItems.indexWhere(
(item) => item.id == id,
);
print(updatedIndex.toString());
if (updatedIndex == -1) {
return;
}
}
bool? navigationResult = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailView(initialItemId: id)),
);
if (navigationResult == true) {
scrollController.jumpTo(0);
}
}
/*
type: mediaTypes[state.typeIndex],
mode: state.modeIndex,
random: state.random,
tag: state.tag,*/

View File

@ -25,7 +25,7 @@ class VideoControlsOverlay extends ConsumerWidget {
children: [ children: [
Positioned( Positioned(
right: 12, right: 12,
top: 12, bottom: 12,
child: _ControlButton( child: _ControlButton(
mediaState.muted ? Icons.volume_off : Icons.volume_up, mediaState.muted ? Icons.volume_off : Icons.volume_up,
() { () {
@ -35,13 +35,6 @@ class VideoControlsOverlay extends ConsumerWidget {
size: 16, size: 16,
), ),
), ),
Positioned(
right: 12,
bottom: 12,
child: _ControlButton(Icons.fullscreen, () {
button();
}, size: 16),
),
Center( Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -89,7 +82,6 @@ class VideoControlsOverlay extends ConsumerWidget {
bottom: 12, bottom: 12,
child: Text( child: Text(
'${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}', '${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
style: TextStyle(color: Colors.white),
), ),
), ),
Listener( Listener(
@ -153,7 +145,7 @@ class _ControlButton extends StatelessWidget {
color: Colors.black.withValues(alpha: 0.4), color: Colors.black.withValues(alpha: 0.4),
), ),
child: IconButton( child: IconButton(
icon: Icon(icon, color: Colors.white, size: size), icon: Icon(icon, size: size),
onPressed: onPressed, onPressed: onPressed,
), ),
); );

View File

@ -240,6 +240,14 @@ 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:
@ -304,6 +312,14 @@ 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.5+35 version: 1.1.9+39
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta
@ -41,6 +41,7 @@ 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: