Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
346e447d5e | |||
f7777821fd | |||
ffbde73300 | |||
836a0886e2 | |||
1cd10b3809 | |||
43c42ac0d5 |
@ -1,11 +1,11 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:label="f0ckapp"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@ -29,12 +29,26 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="false"/>
|
||||
<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>
|
||||
<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>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 3.5 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 839 B |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 4.5 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 446 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 5.2 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 75 KiB |
After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 133 KiB |
After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/play_store_512.png
Normal file
After Width: | Height: | Size: 345 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 447 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 192 KiB |
Before Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 14 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.7 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 7.0 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
Normal file
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 2.2 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
Normal file
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
Normal file
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 43 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
Normal file
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 36 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
Normal file
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 1.1 MiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -1,3 +1,4 @@
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@ -11,12 +12,14 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
await AppVersion.init();
|
||||
final Uri? initialUri = await AppLinks().getInitialLink();
|
||||
|
||||
runApp(ProviderScope(child: const F0ckApp()));
|
||||
runApp(ProviderScope(child: F0ckApp(initialUri: initialUri)));
|
||||
}
|
||||
|
||||
class F0ckApp extends ConsumerWidget {
|
||||
const F0ckApp({super.key});
|
||||
final Uri? initialUri;
|
||||
const F0ckApp({super.key, this.initialUri});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@ -25,7 +28,7 @@ class F0ckApp extends ConsumerWidget {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ref.watch(themeNotifierProvider),
|
||||
home: const MediaGrid(),
|
||||
home: MediaGrid(initialUri: initialUri),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -106,14 +106,16 @@ class MediaNotifier extends StateNotifier<MediaState> {
|
||||
state = state.replace(mediaItems: updated);
|
||||
}
|
||||
|
||||
Future<void> loadMedia() async {
|
||||
if (state.isLoading) return;
|
||||
Future<void> loadMedia({int? id}) async {
|
||||
//if (state.isLoading) return;
|
||||
if (id != null) {
|
||||
print('requested id: ${id.toString()}');
|
||||
}
|
||||
|
||||
state = state.replace(isLoading: true);
|
||||
try {
|
||||
final older = state.mediaItems.isNotEmpty
|
||||
? state.mediaItems.last.id
|
||||
: null;
|
||||
final older =
|
||||
id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
|
||||
final newMedia = await fetchMedia(
|
||||
older: older,
|
||||
type: mediaTypes[state.typeIndex],
|
||||
|
@ -1,8 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.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> {
|
||||
final FlutterSecureStorage secureStorage;
|
||||
|
||||
@ -11,48 +21,37 @@ class ThemeNotifier extends StateNotifier<ThemeData> {
|
||||
}
|
||||
|
||||
Future<void> _loadTheme() async {
|
||||
String? savedThemeName = await secureStorage.read(key: 'theme');
|
||||
if (savedThemeName != null) {
|
||||
final customTheme = themes.firstWhere(
|
||||
(t) => t.name == savedThemeName,
|
||||
orElse: () => CustomTheme(name: 'f0ck', theme: f0ckTheme),
|
||||
);
|
||||
state = customTheme.theme;
|
||||
try {
|
||||
String? savedThemeName = await secureStorage.read(key: 'theme');
|
||||
if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
|
||||
state = themeMap[savedThemeName]!;
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('Fehler beim Laden des Themes: $error');
|
||||
state = f0ckTheme;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTheme(String themeName) async {
|
||||
await secureStorage.write(key: 'theme', value: themeName);
|
||||
final newTheme = themes.firstWhere(
|
||||
(t) => t.name == themeName,
|
||||
orElse: () => CustomTheme(name: 'f0ck', theme: f0ckTheme),
|
||||
);
|
||||
state = newTheme.theme;
|
||||
try {
|
||||
await secureStorage.write(key: 'theme', value: themeName);
|
||||
state = themeMap[themeName] ?? f0ckTheme;
|
||||
} catch (error) {
|
||||
debugPrint('Fehler beim Aktualisieren des Themes: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final themeNotifierProvider = StateNotifierProvider<ThemeNotifier, ThemeData>((
|
||||
ref,
|
||||
) {
|
||||
return ThemeNotifier(secureStorage: FlutterSecureStorage());
|
||||
return ThemeNotifier(
|
||||
secureStorage: const FlutterSecureStorage(
|
||||
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(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: const Color(0xFF9FFF00),
|
||||
@ -74,13 +73,53 @@ final ThemeData f0ckTheme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Colors.white),
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: const Color(0xFF9FFF00),
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF000000),
|
||||
backgroundColor: const Color(0xFF9FFF00),
|
||||
),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)),
|
||||
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
|
||||
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),
|
||||
),
|
||||
);
|
||||
|
||||
@ -105,9 +144,11 @@ final ThemeData paperTheme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Color(0xFF000000)),
|
||||
bodyMedium: TextStyle(color: Color(0xFF000000)),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: const Color(0xFF000000),
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFFFFFFF),
|
||||
backgroundColor: const Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -132,13 +173,15 @@ final ThemeData orangeTheme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Colors.white),
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: const Color(0xFFFF6F00),
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: const Color(0xFFFF6F00),
|
||||
),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)),
|
||||
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
|
||||
),
|
||||
);
|
||||
|
||||
@ -165,9 +208,11 @@ final ThemeData amoledTheme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Colors.white),
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: const Color(0xFFFFFFFF),
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: const Color(0xFFFFFFFF),
|
||||
),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF1D1C1C)),
|
||||
@ -195,13 +240,15 @@ final ThemeData f0ck95Theme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Colors.black),
|
||||
bodyMedium: TextStyle(color: Colors.black),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: Colors.black,
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)),
|
||||
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
|
||||
),
|
||||
);
|
||||
|
||||
@ -225,12 +272,14 @@ final ThemeData f0ck95dTheme = ThemeData(
|
||||
bodyLarge: TextStyle(color: Colors.white),
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
),
|
||||
buttonTheme: ButtonThemeData(
|
||||
buttonColor: Colors.white,
|
||||
textTheme: ButtonTextTheme.primary,
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
scrollbarTheme: ScrollbarThemeData(
|
||||
thumbColor: WidgetStatePropertyAll<Color>(Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStatePropertyAll<Color>(Color(0xFF424242)),
|
||||
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
|
||||
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
|
||||
),
|
||||
);
|
||||
|
@ -48,16 +48,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
|
||||
void _preloadAdjacentMedia(int index) async {
|
||||
final mediaState = ref.read(mediaProvider);
|
||||
if (index + 1 < mediaState.mediaItems.length) {
|
||||
final nextUrl = mediaState.mediaItems[index + 1].mediaUrl;
|
||||
if (await DefaultCacheManager().getFileFromCache(nextUrl) == null) {
|
||||
await DefaultCacheManager().downloadFile(nextUrl);
|
||||
}
|
||||
}
|
||||
if (index - 1 >= 0) {
|
||||
final prevUrl = mediaState.mediaItems[index - 1].mediaUrl;
|
||||
if (await DefaultCacheManager().getFileFromCache(prevUrl) == null) {
|
||||
await DefaultCacheManager().downloadFile(prevUrl);
|
||||
for (int offset in [-1, 1]) {
|
||||
final adjacentIndex = index + offset;
|
||||
if (adjacentIndex >= 0 && adjacentIndex < mediaState.mediaItems.length) {
|
||||
final url = mediaState.mediaItems[adjacentIndex].mediaUrl;
|
||||
if (await DefaultCacheManager().getFileFromCache(url) == null) {
|
||||
await DefaultCacheManager().downloadFile(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,9 +103,15 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(mediaState.muted ? Icons.volume_off : Icons.volume_up),
|
||||
icon: Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
mediaNotifier.toggleMute();
|
||||
// wip
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download),
|
||||
onPressed: () {
|
||||
// wip
|
||||
},
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
@ -149,14 +152,14 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
value: 'direct_link',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: const Text('Link zum Bild'),
|
||||
title: const Text('Link zur Datei'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'post_link',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.article),
|
||||
title: const Text('Link zum Post'),
|
||||
title: const Text('Link zum f0ck'),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -182,14 +185,14 @@ class _DetailViewState extends ConsumerState<DetailView> {
|
||||
),
|
||||
persistentFooterButtons: mediaState.tag != null
|
||||
? [
|
||||
InputChip(
|
||||
label: Text(mediaState.tag!),
|
||||
//backgroundColor: const Color(0xFF090909),
|
||||
//labelStyle: const TextStyle(color: Colors.white),
|
||||
onDeleted: () {
|
||||
mediaNotifier.setTag(null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
Center(
|
||||
child: InputChip(
|
||||
label: Text(mediaState.tag!),
|
||||
onDeleted: () {
|
||||
mediaNotifier.setTag(null);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
|
@ -1,18 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:f0ckapp/screens/DetailView.dart';
|
||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
||||
import 'package:f0ckapp/utils/AppVersion.dart';
|
||||
import 'package:f0ckapp/utils/ParseDeepLink.dart';
|
||||
import 'package:f0ckapp/providers/ThemeProvider.dart';
|
||||
|
||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||
|
||||
class MediaGrid extends ConsumerStatefulWidget {
|
||||
const MediaGrid({super.key});
|
||||
final Uri? initialUri = null;
|
||||
const MediaGrid({super.key, required initialUri});
|
||||
|
||||
@override
|
||||
ConsumerState<MediaGrid> createState() => _MediaGridState();
|
||||
@ -25,6 +28,8 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
final appLinks = AppLinks();
|
||||
|
||||
int _calculateCrossAxisCount(BuildContext context, int defaultCount) {
|
||||
return defaultCount == 0
|
||||
? (MediaQuery.of(context).size.width / 110).clamp(3, 5).toInt()
|
||||
@ -43,6 +48,23 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
ref.read(mediaProvider.notifier).loadMedia();
|
||||
}
|
||||
});
|
||||
|
||||
appLinks.uriLinkStream.listen((Uri uri) async {
|
||||
final parsedResult = parseDeepLink(uri);
|
||||
if (parsedResult == null) return;
|
||||
if (parsedResult['route'] != 'complex') return;
|
||||
final params = parsedResult['params'] as Map<String, String>;
|
||||
await handleComplexDeepLink(params, context, ref, _scrollController);
|
||||
});
|
||||
|
||||
//print('initial: ${parseDeepLink(widget.initialUri)}');
|
||||
Future.microtask(() async {
|
||||
final initparsedResult = parseDeepLink(widget.initialUri);
|
||||
if (initparsedResult == null) return;
|
||||
if (initparsedResult['route'] != 'complex') return;
|
||||
final initparams = initparsedResult['params'] as Map<String, String>;
|
||||
await handleComplexDeepLink(initparams, context, ref, _scrollController);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -61,15 +83,21 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
spacing: 15,
|
||||
children: [
|
||||
Image.asset('assets/images/f0ck_small.webp', fit: BoxFit.fitHeight),
|
||||
Text(
|
||||
'fApp',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
],
|
||||
title: GestureDetector(
|
||||
child: Row(
|
||||
spacing: 10,
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/f0ck_small.webp',
|
||||
fit: BoxFit.fitHeight,
|
||||
),
|
||||
Text('fApp', style: TextStyle(fontSize: 24)),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
mediaNotifier.setTag(null);
|
||||
_scrollController.jumpTo(0);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
@ -202,17 +230,19 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
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 isSelected = currentTheme == themeData.theme;
|
||||
final isSelected = currentTheme == themeData;
|
||||
return ListTile(
|
||||
title: Text(themeData.name),
|
||||
title: Text(themeName),
|
||||
selected: isSelected,
|
||||
selectedTileColor: Colors.blue.withValues(alpha: 0.2),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(themeNotifierProvider.notifier)
|
||||
.updateTheme(themeData.name);
|
||||
.updateTheme(themeName);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
@ -220,7 +250,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: Text('v${AppVersion.version}'),
|
||||
onTap: () {
|
||||
@ -236,12 +265,14 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
),
|
||||
persistentFooterButtons: mediaState.tag != null
|
||||
? [
|
||||
InputChip(
|
||||
label: Text(mediaState.tag!),
|
||||
onDeleted: () {
|
||||
mediaNotifier.setTag(null);
|
||||
_scrollController.jumpTo(0);
|
||||
},
|
||||
Center(
|
||||
child: InputChip(
|
||||
label: Text(mediaState.tag!),
|
||||
onDeleted: () {
|
||||
mediaNotifier.setTag(null);
|
||||
_scrollController.jumpTo(0);
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
|
83
lib/utils/ParseDeepLink.dart
Normal file
@ -0,0 +1,83 @@
|
||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
||||
import 'package:f0ckapp/screens/DetailView.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
|
||||
|
||||
class VideoControlsOverlay extends StatelessWidget {
|
||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
||||
|
||||
class VideoControlsOverlay extends ConsumerWidget {
|
||||
final CachedVideoPlayerPlusController controller;
|
||||
final VoidCallback button;
|
||||
|
||||
@ -13,10 +16,25 @@ class VideoControlsOverlay extends StatelessWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
final mediaState = ref.watch(mediaProvider);
|
||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned(
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
child: _ControlButton(
|
||||
mediaState.muted ? Icons.volume_off : Icons.volume_up,
|
||||
() {
|
||||
button();
|
||||
mediaNotifier.toggleMute();
|
||||
},
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -64,7 +82,6 @@ class VideoControlsOverlay extends StatelessWidget {
|
||||
bottom: 12,
|
||||
child: Text(
|
||||
'${_formatDuration(controller.value.position)} / ${_formatDuration(controller.value.duration)}',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
Listener(
|
||||
@ -128,7 +145,7 @@ class _ControlButton extends StatelessWidget {
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: Icon(icon, color: Colors.white, size: size),
|
||||
icon: Icon(icon, size: size),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
);
|
||||
|
40
pubspec.lock
@ -1,6 +1,38 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.0"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_platform_interface
|
||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -240,6 +272,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|