Compare commits

...

10 Commits

Author SHA1 Message Date
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
836a0886e2 Icon Schmicon
All checks were successful
Flutter Schmutter / build (push) Successful in 3m33s
2025-06-07 20:41:06 +02:00
1cd10b3809 v1.1.5+35
- overlay buttons
- encrypted storage
- downloadbutton (wip)
2025-06-07 20:32:24 +02:00
43c42ac0d5 v1.1.4+34
All checks were successful
Flutter Schmutter / build (push) Successful in 3m27s
2025-06-07 16:52:30 +02:00
bf4e0fa493 v1.1.3+33
All checks were successful
Flutter Schmutter / build (push) Successful in 3m38s
2025-06-07 16:30:49 +02:00
27476fbc1d mute schmute 2025-06-07 12:28:24 +02:00
8e9f0eb1b8 v1.1.1+32
All checks were successful
Flutter Schmutter / build (push) Successful in 6m13s
2025-06-06 19:26:53 +02:00
f083fc8e8f ic_launcher_round 2025-06-06 18:32:59 +02:00
163 changed files with 1119 additions and 3414 deletions

View File

@ -11,12 +11,12 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {

View File

@ -1,5 +1,6 @@
<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"
@ -28,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

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,42 +1,36 @@
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:f0ckapp/screens/MediaGrid.dart';
import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppVersion.init();
final Uri? initialUri = await AppLinks().getInitialLink();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => ThemeProvider()),
ChangeNotifierProvider(create: (context) => MediaProvider())
],
child: F0ckApp()
)
);
runApp(ProviderScope(child: F0ckApp(initialUri: initialUri)));
}
class F0ckApp extends StatelessWidget {
const F0ckApp({super.key});
class F0ckApp extends ConsumerWidget {
final Uri? initialUri;
const F0ckApp({super.key, this.initialUri});
@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: themeProvider.themeData,
home: Scaffold(
body: MediaGrid(),
),
Widget build(BuildContext context, WidgetRef ref) {
return Consumer(
builder: (context, ref, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ref.watch(themeNotifierProvider),
home: MediaGrid(initialUri: initialUri),
);
},
);
}
}

View File

@ -1,95 +1,144 @@
import 'package:f0ckapp/services/Api.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/services/Api.dart';
class MediaProvider extends ChangeNotifier {
int _typeid = 0;
int _mode = 0;
bool _random = false;
String? _tag;
int _crossAxisCount = 0;
List<MediaItem> _mediaItems = [];
bool _isLoading = false;
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
const _unsetTag = Object();
List<String> types = ["alles", "image", "video", "audio"];
List<String> modes = ["sfw", "nsfw", "untagged", "all"];
class MediaState {
final int typeIndex;
final int modeIndex;
final bool random;
final String? tag;
final int crossAxisCount;
final List<MediaItem> mediaItems;
final bool isLoading;
final bool muted;
String get type => types[_typeid];
int get typeid => _typeid;
int get mode => _mode;
bool get random => _random;
String? get tag => _tag;
int get crossAxisCount => _crossAxisCount;
List<MediaItem> get mediaItems => _mediaItems;
bool get isLoading => _isLoading;
Function get resetMedia => _resetMedia;
const MediaState({
this.typeIndex = 0,
this.modeIndex = 0,
this.random = false,
this.tag,
this.crossAxisCount = 0,
this.mediaItems = const [],
this.isLoading = false,
this.muted = false,
});
void setType(String type) {
_typeid = types.indexOf(type);
_resetMedia();
MediaState replace({
int? typeIndex,
int? modeIndex,
bool? random,
Object? tag = _unsetTag,
int? crossAxisCount,
List<MediaItem>? mediaItems,
bool? isLoading,
bool? muted,
}) {
return MediaState(
typeIndex: typeIndex ?? this.typeIndex,
modeIndex: modeIndex ?? this.modeIndex,
random: random ?? this.random,
tag: identical(tag, _unsetTag) ? this.tag : tag as String?,
crossAxisCount: crossAxisCount ?? this.crossAxisCount,
mediaItems: mediaItems ?? this.mediaItems,
isLoading: isLoading ?? this.isLoading,
muted: muted ?? this.muted,
);
}
}
class MediaNotifier extends StateNotifier<MediaState> {
final _storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
MediaNotifier() : super(const MediaState()) {
_loadMutedState();
}
void setMode(int mode) {
_mode = mode;
_resetMedia();
Future<void> _loadMutedState() async {
final storedMuted = await _storage.read(key: 'muted');
final isMuted = storedMuted == 'true';
state = state.replace(muted: isMuted);
}
Future<void> _saveMutedState() async {
await _storage.write(key: 'muted', value: state.muted.toString());
}
void setType(String type) {
final newIndex = mediaTypes.indexOf(type);
state = state.replace(typeIndex: newIndex);
resetMedia();
}
void setMode(int modeIndex) {
state = state.replace(modeIndex: modeIndex);
resetMedia();
}
void toggleRandom() {
_random = !_random;
_resetMedia();
state = state.replace(random: !state.random);
resetMedia();
}
void setTag(String? tag) {
_tag = tag;
_resetMedia();
state = state.replace(tag: tag);
resetMedia();
}
void setCrossAxisCount(int crossAxisCount) {
_crossAxisCount = crossAxisCount;
notifyListeners();
void setCrossAxisCount(int count) {
state = state.replace(crossAxisCount: count);
}
void setMediaItems(List<MediaItem> mediaItems) {
if (_mediaItems != mediaItems) {
_mediaItems.clear();
_mediaItems.addAll(mediaItems);
notifyListeners();
}
}
void addMediaItems(List<MediaItem> newItems) {
_mediaItems.addAll(newItems);
notifyListeners();
}
void _resetMedia() {
_mediaItems.clear();
notifyListeners();
void resetMedia() {
state = state.replace(mediaItems: []);
loadMedia();
}
Future<void> loadMedia({bool notify = true}) async {
if (_isLoading) return;
_isLoading = true;
if (notify) notifyListeners();
void addMediaItems(List<MediaItem> newItems) {
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
state = state.replace(mediaItems: updated);
}
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 =
id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
final newMedia = await fetchMedia(
older: _mediaItems.isNotEmpty ? _mediaItems.last.id : null,
type: type,
mode: mode,
random: random,
tag: tag,
older: older,
type: mediaTypes[state.typeIndex],
mode: state.modeIndex,
random: state.random,
tag: state.tag,
);
if(_mediaItems != newMedia) {
if (newMedia.isNotEmpty) {
addMediaItems(newMedia);
if (notify) notifyListeners();
}
} catch (e) {
debugPrint('Fehler beim Laden der Medien: $e');
print('Fehler beim Laden der Medien: $e');
} finally {
_isLoading = false;
state = state.replace(isLoading: false);
}
}
void toggleMute() {
state = state.replace(muted: !state.muted);
_saveMutedState();
}
}
final mediaProvider = StateNotifierProvider<MediaNotifier, MediaState>(
(ref) => MediaNotifier(),
);

View File

@ -1,132 +1,285 @@
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;
ThemeNotifier({required this.secureStorage}) : super(f0ckTheme) {
_loadTheme();
}
Future<void> _loadTheme() async {
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 {
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: const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
),
);
});
final ThemeData f0ckTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: Color(0xFF9FFF00),
scaffoldBackgroundColor: Color(0xFF000000),
colorScheme: ColorScheme.dark(
primaryColor: const Color(0xFF9FFF00),
scaffoldBackgroundColor: const Color(0xFF000000),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF9FFF00),
secondary: Color(0xFF262626),
surface: Color(0xFF232323),
onPrimary: Color(0xFF000000),
onSecondary: Color(0xFFFFFFFF),
onSurface: Color(0xFFFFFFFF),
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: AppBarTheme(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2B2B2B),
foregroundColor: Color(0xFF9FFF00),
elevation: 2,
),
textTheme: TextTheme(
bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFFFFFFFF)),
bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFFFFFFFF)),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF9FFF00),
textTheme: ButtonTextTheme.primary,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF000000),
backgroundColor: const Color(0xFF9FFF00),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all(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),
),
);
final ThemeData paperTheme = ThemeData(
brightness: Brightness.light,
primaryColor: Color(0xFF000000),
scaffoldBackgroundColor: Color(0xFFFFFFFF),
colorScheme: ColorScheme.light(
primaryColor: const Color(0xFF000000),
scaffoldBackgroundColor: const Color(0xFFFFFFFF),
colorScheme: const ColorScheme.light(
primary: Color(0xFF000000),
secondary: Color(0xFF262626),
surface: Color(0xFFFFFFFF),
onPrimary: Color(0xFFFFFFFF),
onSecondary: Color(0xFFFFFFFF),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Color(0xFF000000),
),
appBarTheme: AppBarTheme(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFFFFFFF),
foregroundColor: Color(0xFF000000),
elevation: 0,
),
textTheme: TextTheme(
bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFF000000)),
bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFF000000)),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Color(0xFF000000)),
bodyMedium: TextStyle(color: Color(0xFF000000)),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF000000),
textTheme: ButtonTextTheme.primary,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFFFFFFFF),
backgroundColor: const Color(0xFF000000),
),
),
);
final ThemeData orangeTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFF6F00),
scaffoldBackgroundColor: const Color(0xFF171717),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFF6F00),
secondary: Color(0xFF262626),
surface: Color(0xFF232323),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2B2B2B),
foregroundColor: Color(0xFFFF6F00),
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0xFFFF6F00),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData amoledTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFFFFFF),
scaffoldBackgroundColor: const Color(0xFF000000),
canvasColor: const Color(0xFF000000),
cardColor: const Color(0xFF000000),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFFFFFF),
secondary: Color(0xFF1F1F1F),
surface: Color(0xFF000000),
onPrimary: Color(0xFF000000),
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF000000),
foregroundColor: Colors.white,
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: const Color(0xFFFFFFFF),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF1D1C1C)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF000000)),
),
);
final ThemeData f0ck95Theme = ThemeData(
brightness: Brightness.light,
primaryColor: Color(0xFFC0C0C0),
scaffoldBackgroundColor: Color(0xFF008080),
colorScheme: ColorScheme.light(
primaryColor: const Color(0xFFC0C0C0),
scaffoldBackgroundColor: const Color(0xFF008080),
colorScheme: const ColorScheme.light(
primary: Color(0xFFC0C0C0),
secondary: Color(0xFF808080),
surface: Color(0xFFC0C0C0),
onPrimary: Color(0xFF000000),
onSecondary: Color(0xFFFFFFFF),
onPrimary: Colors.black,
onSecondary: Colors.white,
),
appBarTheme: AppBarTheme(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFC0C0C0),
foregroundColor: Color(0xFF000000),
foregroundColor: Colors.black,
elevation: 2,
),
textTheme: TextTheme(
bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFF000000)),
bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFF000000)),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black),
bodyMedium: TextStyle(color: Colors.black),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFF000000),
textTheme: ButtonTextTheme.primary,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.black,
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all(Color(0xFF424242)),
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData f0ck95dTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: Color(0xFFFFFFFF),
scaffoldBackgroundColor: Color(0xFF0E0F0F),
colorScheme: ColorScheme.dark(
primaryColor: const Color(0xFFFFFFFF),
scaffoldBackgroundColor: const Color(0xFF0E0F0F),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFFFFFF),
secondary: Color(0xFFC0C0C0),
surface: Color(0xFF333131),
onPrimary: Color(0xFF000000),
onSecondary: Color(0xFFFFFFFF),
onPrimary: Colors.black,
onSecondary: Colors.white,
),
appBarTheme: AppBarTheme(
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF0B0A0A),
foregroundColor: Color(0xFFFFFFFF),
foregroundColor: Colors.white,
elevation: 2,
),
textTheme: TextTheme(
bodyLarge: TextStyle(fontFamily: 'VCR', color: Color(0xFFFFFFFF)),
bodyMedium: TextStyle(fontFamily: 'monospace', color: Color(0xFFFFFFFF)),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
buttonTheme: ButtonThemeData(
buttonColor: Color(0xFFFFFFFF),
textTheme: ButtonTextTheme.primary,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all(Color(0xFF424242)),
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
class ThemeProvider extends ChangeNotifier {
ThemeData _themeData = f0ck95dTheme;
ThemeData get themeData => _themeData;
/*void toggleTheme() {
_themeData = _themeData == lightTheme ? darkTheme : lightTheme;
notifyListeners();
}*/
}

View File

@ -2,27 +2,28 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/widgets/VideoWidget.dart';
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
import 'package:f0ckapp/utils/PageTransformer.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:share_plus/share_plus.dart';
class DetailView extends StatefulWidget {
class DetailView extends ConsumerStatefulWidget {
final int initialItemId;
const DetailView({super.key, required this.initialItemId});
@override
State createState() => _DetailViewState();
ConsumerState<DetailView> createState() => _DetailViewState();
}
class _DetailViewState extends State<DetailView> {
class _DetailViewState extends ConsumerState<DetailView> {
late PageController _pageController;
bool isLoading = false;
int _currentIndex = 0;
@ -30,15 +31,14 @@ class _DetailViewState extends State<DetailView> {
@override
void initState() {
super.initState();
final provider = Provider.of<MediaProvider>(context, listen: false);
final initialIndex = provider.mediaItems.indexWhere(
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);
});
@ -47,18 +47,14 @@ class _DetailViewState extends State<DetailView> {
}
void _preloadAdjacentMedia(int index) async {
final provider = Provider.of<MediaProvider>(context, listen: false);
if (index + 1 < provider.mediaItems.length) {
final nextUrl = provider.mediaItems[index + 1].mediaUrl;
if (await DefaultCacheManager().getFileFromCache(nextUrl) == null) {
await DefaultCacheManager().downloadFile(nextUrl);
}
}
if (index - 1 >= 0) {
final prevUrl = provider.mediaItems[index - 1].mediaUrl;
if (await DefaultCacheManager().getFileFromCache(prevUrl) == null) {
await DefaultCacheManager().downloadFile(prevUrl);
final mediaState = ref.read(mediaProvider);
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);
}
}
}
}
@ -67,19 +63,8 @@ class _DetailViewState extends State<DetailView> {
if (isLoading) return;
setState(() => isLoading = true);
final provider = Provider.of<MediaProvider>(context, listen: false);
try {
final newMedia = await fetchMedia(
older: provider.mediaItems.last.id,
type: provider.type,
mode: provider.mode,
random: provider.random,
tag: provider.tag,
);
if (mounted && newMedia.isNotEmpty) {
setState(() => provider.mediaItems.addAll(newMedia));
}
await ref.read(mediaProvider.notifier).loadMedia();
} catch (e) {
_showError("Fehler beim Laden der Medien: $e");
} finally {
@ -89,42 +74,69 @@ class _DetailViewState extends State<DetailView> {
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<MediaProvider>(context);
final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier);
if (mediaState.mediaItems.isEmpty) {
return Scaffold(
appBar: AppBar(),
body: const Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(
'f0ck #${provider.mediaItems.elementAt(_currentIndex).id.toString()}',
),
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
actions: [
IconButton(
icon: Icon(Icons.fullscreen),
onPressed: () {
// wip
},
),
IconButton(
icon: Icon(Icons.download),
onPressed: () {
// wip
},
),
PopupMenuButton<String>(
onSelected: (value) async {
final item = provider.mediaItems.elementAt(_currentIndex);
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));
await SharePlus.instance.share(
ShareParams(text: item.mediaUrl),
);
break;
case 'post_link':
await SharePlus.instance.share(ShareParams(text: item.postUrl));
await SharePlus.instance.share(
ShareParams(text: item.postUrl),
);
break;
}
},
@ -132,26 +144,26 @@ class _DetailViewState extends State<DetailView> {
PopupMenuItem(
value: 'media',
child: ListTile(
leading: Icon(Icons.image),
title: Text('Als Datei'),
leading: const Icon(Icons.image),
title: const Text('Als Datei'),
),
),
PopupMenuItem(
value: 'direct_link',
child: ListTile(
leading: Icon(Icons.link),
title: Text('Link zum Bild'),
leading: const Icon(Icons.link),
title: const Text('Link zur Datei'),
),
),
PopupMenuItem(
value: 'post_link',
child: ListTile(
leading: Icon(Icons.article),
title: Text('Link zum Post'),
leading: const Icon(Icons.article),
title: const Text('Link zum f0ck'),
),
),
],
icon: Icon(Icons.share),
icon: const Icon(Icons.share),
),
],
),
@ -159,9 +171,8 @@ class _DetailViewState extends State<DetailView> {
children: [
PageTransformer(
controller: _pageController,
pages: provider.mediaItems.map((item) {
int itemIndex = provider.mediaItems.indexOf(item);
pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea(
child: SmartRefreshIndicator(
onRefresh: _loadMoreMedia,
@ -172,16 +183,16 @@ class _DetailViewState extends State<DetailView> {
),
],
),
persistentFooterButtons: provider.tag != null
persistentFooterButtons: mediaState.tag != null
? [
InputChip(
label: Text(provider.tag!),
backgroundColor: const Color(0xFF090909),
labelStyle: const TextStyle(color: Colors.white),
onDeleted: () {
provider.setTag(null);
Navigator.pop(context);
},
Center(
child: InputChip(
label: Text(mediaState.tag!),
onDeleted: () {
mediaNotifier.setTag(null);
Navigator.pop(context);
},
),
),
]
: null,
@ -189,7 +200,7 @@ class _DetailViewState extends State<DetailView> {
}
Widget _buildMediaItem(MediaItem item, bool isActive) {
final provider = Provider.of<MediaProvider>(context);
final mediaNotifier = ref.read(mediaProvider.notifier);
return SingleChildScrollView(
child: Column(
@ -198,16 +209,11 @@ class _DetailViewState extends State<DetailView> {
CachedNetworkImage(
imageUrl: item.mediaUrl,
fit: BoxFit.contain,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
else
VideoWidget(details: item, isActive: isActive),
/*const SizedBox(height: 20),
Text(
'f0ck #${item.id.toString()}',
style: const TextStyle(color: Colors.white, fontSize: 18),
),*/
const SizedBox(height: 10, width: double.infinity),
Wrap(
alignment: WrapAlignment.center,
@ -217,7 +223,7 @@ class _DetailViewState extends State<DetailView> {
onPressed: () {
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
setState(() {
provider.setTag(tag.tag);
mediaNotifier.setTag(tag.tag);
Navigator.pop(context, true);
});
},

View File

@ -1,37 +1,34 @@
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:provider/provider.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';
class MediaGrid extends StatefulWidget {
const MediaGrid({super.key});
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
class MediaGrid extends ConsumerStatefulWidget {
final Uri? initialUri = null;
const MediaGrid({super.key, required initialUri});
@override
State createState() => _MediaGridState();
ConsumerState<MediaGrid> createState() => _MediaGridState();
}
class _MediaGridState extends State<MediaGrid> {
class _MediaGridState extends ConsumerState<MediaGrid> {
final ScrollController _scrollController = ScrollController();
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
@override
void initState() {
super.initState();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final provider = Provider.of<MediaProvider>(context, listen: false);
Future.microtask(() {
provider.loadMedia();
});
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
provider.loadMedia(notify: false);
}
});
}
final appLinks = AppLinks();
int _calculateCrossAxisCount(BuildContext context, int defaultCount) {
return defaultCount == 0
@ -39,32 +36,83 @@ class _MediaGridState extends State<MediaGrid> {
: defaultCount;
}
@override
void initState() {
super.initState();
Future.microtask(() {
ref.read(mediaProvider.notifier).loadMedia();
});
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
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
void dispose() {
_scrollController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<MediaProvider>(context);
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold(
key: scaffoldKey,
key: _scaffoldKey,
appBar: AppBar(
title: Image.asset(
'assets/images/f0ck_small.webp',
fit: BoxFit.fitHeight,
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(
icon: Icon(
provider.random ? Icons.shuffle_on_outlined : Icons.shuffle,
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
),
onPressed: () {
provider.toggleRandom();
mediaNotifier.toggleRandom();
_scrollController.jumpTo(0);
},
),
IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
scaffoldKey.currentState?.openEndDrawer();
_scaffoldKey.currentState?.openEndDrawer();
},
),
],
@ -74,32 +122,11 @@ class _MediaGridState extends State<MediaGrid> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text('type: '),
const Text('type: '),
DropdownButton<String>(
// type
value: provider.type,
value: mediaTypes[mediaState.typeIndex],
isDense: true,
//icon: SizedBox.shrink(),
items: provider.types.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
provider.setType(newValue);
_scrollController.jumpTo(0);
}
},
),
Text('mode: '),
DropdownButton<String>(
// mode
value: provider.modes[provider.mode],
isDense: true,
//icon: SizedBox.shrink(),
items: provider.modes.map((String value) {
items: mediaTypes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
@ -107,7 +134,24 @@ class _MediaGridState extends State<MediaGrid> {
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
provider.setMode(provider.modes.indexOf(newValue));
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);
}
},
@ -120,7 +164,7 @@ class _MediaGridState extends State<MediaGrid> {
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: BoxDecoration(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/menu.webp'),
fit: BoxFit.cover,
@ -129,94 +173,169 @@ class _MediaGridState extends State<MediaGrid> {
),
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 themeName = entry.key;
final themeData = entry.value;
final currentTheme = ref.watch(themeNotifierProvider);
final 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(
SnackBar(content: Text('jooong lass das, hier ist nichts')),
const SnackBar(
content: Text('jooong lass das, hier ist nichts'),
),
);
},
),
],
),
),
persistentFooterButtons: provider.tag != null
persistentFooterButtons: mediaState.tag != null
? [
InputChip(
label: Text(provider.tag!),
backgroundColor: const Color(0xFF090909),
labelStyle: const TextStyle(color: Colors.white),
onDeleted: () {
provider.setTag(null);
_scrollController.jumpTo(0);
},
Center(
child: InputChip(
label: Text(mediaState.tag!),
onDeleted: () {
mediaNotifier.setTag(null);
_scrollController.jumpTo(0);
},
),
),
]
: null,
body: RefreshIndicator(
onRefresh: () async {
await provider.resetMedia();
mediaNotifier.resetMedia();
_scrollController.jumpTo(0);
},
child: Consumer<MediaProvider>(
builder: (context, mediaProvider, child) {
return GridView.builder(
key: PageStorageKey('mediaGrid'),
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _calculateCrossAxisCount(
context,
provider.crossAxisCount,
),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount:
provider.mediaItems.length + (provider.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= provider.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = provider.mediaItems[index];
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: (context, index) {
if (index >= mediaState.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaState.mediaItems[index];
return InkWell(
onTap: () async {
bool? ret = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DetailView(initialItemId: item.id),
),
);
if (ret != null && ret) {
_scrollController.jumpTo(0);
}
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
CachedNetworkImage(
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => SizedBox.shrink(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Align(
alignment: FractionalOffset.bottomRight,
child: Icon(
Icons.square,
color: switch (item.mode) {
1 => Colors.green,
2 => Colors.red,
_ => Colors.yellow,
},
size: 15.0,
),
),
],
return InkWell(
onTap: () async {
bool? ret = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailView(initialItemId: item.id),
),
);
if (ret != null && ret) {
_scrollController.jumpTo(0);
}
},
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

@ -1,8 +1,12 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/MediaItem.dart';
final storage = FlutterSecureStorage();
Future<List<MediaItem>> fetchMedia({
int? older,
String? type,
@ -43,3 +47,23 @@ Future<MediaItem> fetchMediaDetail(int itemId) async {
);
}
}
Future<bool> login(String username, String password) async {
final Uri url = Uri.parse('https://api.f0ck.me/login');
final response = await http.post(
url,
body: {'username': username, 'password': password},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final token = data['token'];
await storage.write(key: "token", value: token);
return true;
} else {
return false;
}
}

View 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);
}
}

View File

@ -1,7 +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;
@ -12,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,
@ -63,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(
@ -127,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,
),
);

View File

@ -1,21 +1,26 @@
import 'package:flutter/material.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/widgets/VideoOverlay.dart';
import 'dart:async';
class VideoWidget extends StatefulWidget {
import 'package:flutter/material.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/widgets/VideoOverlay.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
class VideoWidget extends ConsumerStatefulWidget {
final MediaItem details;
final bool isActive;
const VideoWidget({super.key, required this.details, required this.isActive});
@override
State createState() => _VideoWidgetState();
ConsumerState<VideoWidget> createState() => _VideoWidgetState();
}
class _VideoWidgetState extends State<VideoWidget> {
class _VideoWidgetState extends ConsumerState<VideoWidget> {
late CachedVideoPlayerPlusController _controller;
bool _showControls = false;
Timer? _hideControlsTimer;
@ -34,10 +39,26 @@ class _VideoWidgetState extends State<VideoWidget> {
setState(() {});
_controller.addListener(() => setState(() {}));
if (widget.isActive) {
_controller.play();
}
_controller.setLooping(true);
final muted = ref.read(mediaProvider).muted;
_controller.setVolume(muted ? 0.0 : 1.0);
}
@override
void didUpdateWidget(covariant VideoWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive != oldWidget.isActive) {
if (widget.isActive) {
_controller.play();
} else {
_controller.pause();
}
}
}
@override
@ -47,24 +68,13 @@ class _VideoWidgetState extends State<VideoWidget> {
super.dispose();
}
@override
void didUpdateWidget(VideoWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isActive) {
_controller.play();
} else {
_controller.pause();
}
}
void _onTap({bool ctrlButton = false}) {
if (!ctrlButton) {
setState(() => _showControls = !_showControls);
}
if (_showControls) {
_hideControlsTimer?.cancel();
_hideControlsTimer = Timer(Duration(seconds: 2), () {
_hideControlsTimer = Timer(const Duration(seconds: 2), () {
setState(() => _showControls = false);
});
}
@ -72,6 +82,12 @@ class _VideoWidgetState extends State<VideoWidget> {
@override
Widget build(BuildContext context) {
final muted = ref.watch(mediaProvider).muted;
if (_controller.value.isInitialized &&
_controller.value.volume != (muted ? 0.0 : 1.0)) {
_controller.setVolume(muted ? 0.0 : 1.0);
}
bool isAudio = widget.details.mime.startsWith('audio');
return Column(
@ -91,7 +107,7 @@ class _VideoWidgetState extends State<VideoWidget> {
imageUrl: widget.details.coverUrl,
fit: BoxFit.cover,
placeholder: (context, url) =>
CircularProgressIndicator(),
const CircularProgressIndicator(),
errorWidget: (context, url, error) => Image.asset(
'assets/images/music.webp',
fit: BoxFit.contain,
@ -100,7 +116,7 @@ class _VideoWidgetState extends State<VideoWidget> {
)
: _controller.value.isInitialized
? CachedVideoPlayerPlus(_controller)
: Center(child: CircularProgressIndicator()),
: const Center(child: CircularProgressIndicator()),
),
if (_controller.value.isInitialized && _showControls) ...[
IgnorePointer(

1
linux/.gitignore vendored
View File

@ -1 +0,0 @@
flutter/ephemeral

View File

@ -1,128 +0,0 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
set(BINARY_NAME "f0ckapp")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.f0ckapp")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
cmake_policy(SET CMP0063 NEW)
# Load bundled libraries from the lib/ directory relative to the binary.
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
# Root filesystem for cross-building.
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
endif()
# Define build configuration options.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Debug" CACHE
STRING "Flutter build mode" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Profile" "Release")
endif()
# Compilation settings that should be applied to most targets.
#
# Be cautious about adding new options here, as plugins use this function by
# default. In most cases, you should add new options to specific targets instead
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
# Flutter library and tool build rules.
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
add_subdirectory(${FLUTTER_MANAGED_DIR})
# System-level dependencies.
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Application build; see runner/CMakeLists.txt.
add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
# Only the install-generated bundle's copy of the executable will launch
# correctly, since the resources must in the right relative locations. To avoid
# people trying to run the unbundled copy, put it in a subdirectory instead of
# the default top-level location.
set_target_properties(${BINARY_NAME}
PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
# directory.
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
endif()
# Start with a clean build bundle directory every time.
install(CODE "
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
" COMPONENT Runtime)
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
COMPONENT Runtime)
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
COMPONENT Runtime)
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
install(FILES "${bundled_library}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endforeach(bundled_library)
# Copy the native assets provided by the build.dart from all packages.
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
# Fully re-copy the assets directory on each build to avoid having stale files
# from a previous install.
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
install(CODE "
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
" COMPONENT Runtime)
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
# Install the AOT library on non-Debug builds only.
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
COMPONENT Runtime)
endif()

Some files were not shown because too many files have changed in this diff Show More