Compare commits
14 Commits
v1.0.29+29
...
v1.1.9+39
Author | SHA1 | Date | |
---|---|---|---|
671b3cfbe0 | |||
93fb3536ee | |||
346e447d5e | |||
f7777821fd | |||
ffbde73300 | |||
836a0886e2 | |||
1cd10b3809 | |||
43c42ac0d5 | |||
bf4e0fa493 | |||
27476fbc1d | |||
8e9f0eb1b8 | |||
f083fc8e8f | |||
9a716018fc | |||
f1eb52518b |
@ -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 {
|
||||
|
@ -1,10 +1,12 @@
|
||||
<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:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@ -27,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="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>
|
||||
<!-- 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 |
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 |
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 |
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 |
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 |
BIN
android/app/src/main/res/play_store_512.png
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
assets/images/f0ck_small.webp
Normal file
After Width: | Height: | Size: 1.4 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,42 +1,87 @@
|
||||
import 'package:f0ckapp/providers/MediaProvider.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:go_router/go_router.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:f0ckapp/screens/MediaGrid.dart';
|
||||
import 'package:f0ckapp/screens/DetailView.dart';
|
||||
import 'package:f0ckapp/utils/AppVersion.dart';
|
||||
import 'package:f0ckapp/providers/ThemeProvider.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
await AppVersion.init();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
ChangeNotifierProvider(create: (context) => MediaProvider())
|
||||
],
|
||||
child: F0ckApp()
|
||||
)
|
||||
);
|
||||
runApp(ProviderScope(child: F0ckApp()));
|
||||
}
|
||||
|
||||
class F0ckApp extends StatelessWidget {
|
||||
const F0ckApp({super.key});
|
||||
class F0ckApp extends ConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final themeProvider = Provider.of<ThemeProvider>(context);
|
||||
|
||||
return MaterialApp(
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themeNotifierProvider);
|
||||
return MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: themeProvider.themeData,
|
||||
home: Scaffold(
|
||||
body: MediaGrid(),
|
||||
),
|
||||
routerConfig: _router,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ class MediaItem {
|
||||
String get thumbnailUrl => 'https://f0ck.me/t/$id.webp';
|
||||
String get mediaUrl => 'https://f0ck.me/b/$dest';
|
||||
String get coverUrl => 'https://f0ck.me/ca/$id.webp';
|
||||
String get postUrl => 'https://f0ck.me/$id';
|
||||
}
|
||||
|
||||
class Tag {
|
||||
|
@ -1,93 +1,140 @@
|
||||
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;
|
||||
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,
|
||||
);
|
||||
|
||||
addMediaItems(newMedia);
|
||||
if (newMedia.isNotEmpty) {
|
||||
addMediaItems(newMedia);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Fehler beim Laden der Medien: $e');
|
||||
print('Fehler beim Laden der Medien: $e');
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
if (notify) notifyListeners();
|
||||
state = state.replace(isLoading: false);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleMute() {
|
||||
state = state.replace(muted: !state.muted);
|
||||
_saveMutedState();
|
||||
}
|
||||
}
|
||||
|
||||
final mediaProvider = StateNotifierProvider<MediaNotifier, MediaState>(
|
||||
(ref) => MediaNotifier(),
|
||||
);
|
||||
|
@ -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();
|
||||
}*/
|
||||
}
|
||||
|
@ -1,60 +1,47 @@
|
||||
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:go_router/go_router.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';
|
||||
|
||||
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> {
|
||||
late PageController _pageController;
|
||||
class _DetailViewState extends ConsumerState<DetailView> {
|
||||
PageController? _pageController;
|
||||
bool isLoading = false;
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final provider = Provider.of<MediaProvider>(context, listen: false);
|
||||
|
||||
final initialIndex = provider.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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,19 +50,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 {
|
||||
@ -85,24 +61,128 @@ 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 int itemIndex = mediaState.mediaItems.indexWhere(
|
||||
(item) => item.id == widget.initialItemId,
|
||||
);
|
||||
|
||||
if (itemIndex == -1) {
|
||||
Future.microtask(() {
|
||||
ref
|
||||
.read(mediaProvider.notifier)
|
||||
.loadMedia(id: widget.initialItemId + 50);
|
||||
});
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
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(
|
||||
appBar: AppBar(centerTitle: true, title: const Text('f0ck')),
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
|
||||
automaticallyImplyLeading: false,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
context.canPop() ? context.pop() : context.go('/', extra: true);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
_showError('fullscreen ist wip');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: () {
|
||||
_showError('download ist wip');
|
||||
},
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
final item = mediaState.mediaItems[_currentIndex];
|
||||
switch (value) {
|
||||
case 'media':
|
||||
File file = await DefaultCacheManager().getSingleFile(
|
||||
item.mediaUrl,
|
||||
);
|
||||
Uint8List bytes = await file.readAsBytes();
|
||||
final params = ShareParams(
|
||||
files: [XFile.fromData(bytes, mimeType: item.mime)],
|
||||
);
|
||||
await SharePlus.instance.share(params);
|
||||
break;
|
||||
case 'direct_link':
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(text: item.mediaUrl),
|
||||
);
|
||||
break;
|
||||
case 'post_link':
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(text: item.postUrl),
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'media',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.image),
|
||||
title: const Text('Als Datei'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'direct_link',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.link),
|
||||
title: const Text('Link zur Datei'),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'post_link',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.article),
|
||||
title: const Text('Link zum f0ck'),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.share),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
PageTransformer(
|
||||
controller: _pageController,
|
||||
pages: provider.mediaItems.map((item) {
|
||||
int itemIndex = provider.mediaItems.indexOf(item);
|
||||
|
||||
controller: _pageController!,
|
||||
pages: mediaState.mediaItems.map((item) {
|
||||
int itemIndex = mediaState.mediaItems.indexOf(item);
|
||||
return SafeArea(
|
||||
child: SmartRefreshIndicator(
|
||||
onRefresh: _loadMoreMedia,
|
||||
@ -113,22 +193,16 @@ class _DetailViewState extends State<DetailView> {
|
||||
),
|
||||
],
|
||||
),
|
||||
persistentFooterButtons: provider.tag != null
|
||||
persistentFooterButtons: mediaState.tag != null
|
||||
? [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('tag: '),
|
||||
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: () {
|
||||
ref.read(mediaProvider.notifier).setTag(null);
|
||||
context.go('/', extra: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
@ -136,7 +210,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(
|
||||
@ -145,16 +219,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,
|
||||
@ -164,8 +233,8 @@ class _DetailViewState extends State<DetailView> {
|
||||
onPressed: () {
|
||||
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
|
||||
setState(() {
|
||||
provider.setTag(tag.tag);
|
||||
Navigator.pop(context, true);
|
||||
mediaNotifier.setTag(tag.tag);
|
||||
context.go('/', extra: true);
|
||||
});
|
||||
},
|
||||
label: Text(tag.tag),
|
||||
|
@ -1,66 +1,97 @@
|
||||
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:f0ckapp/screens/DetailView.dart';
|
||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
||||
import 'package:f0ckapp/utils/AppVersion.dart';
|
||||
import 'package:f0ckapp/providers/ThemeProvider.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class MediaGrid extends StatefulWidget {
|
||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||
|
||||
class MediaGrid extends ConsumerStatefulWidget {
|
||||
const MediaGrid({super.key});
|
||||
|
||||
@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>();
|
||||
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
int _calculateCrossAxisCount(BuildContext context, int defaultCount) {
|
||||
return defaultCount == 0
|
||||
? (MediaQuery.of(context).size.width / 110).clamp(3, 5).toInt()
|
||||
: defaultCount;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final provider = Provider.of<MediaProvider>(context, listen: false);
|
||||
|
||||
Future.microtask(() {
|
||||
provider.loadMedia();
|
||||
ref.read(mediaProvider.notifier).loadMedia();
|
||||
});
|
||||
|
||||
_scrollController.addListener(() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200) {
|
||||
provider.loadMedia(notify: false);
|
||||
ref.read(mediaProvider.notifier).loadMedia();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void scrollToTop() {
|
||||
_scrollController.jumpTo(0);
|
||||
@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(
|
||||
//centerTitle: true,
|
||||
title: Text('fApp v${AppVersion.version}'),
|
||||
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();
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -70,32 +101,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),
|
||||
@ -103,7 +113,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);
|
||||
}
|
||||
},
|
||||
@ -116,95 +143,170 @@ class _MediaGridState extends State<MediaGrid> {
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
DrawerHeader(
|
||||
padding: EdgeInsets.all(0),
|
||||
child: Image.asset('assets/images/menu.webp', fit: BoxFit.cover),
|
||||
decoration: const BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage('assets/images/menu.webp'),
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
),
|
||||
child: null,
|
||||
),
|
||||
ExpansionTile(
|
||||
title: const Text('Login'),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
readOnly: true,
|
||||
controller: _usernameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Benutzername',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
readOnly: true,
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Passwort',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("noch nicht implementiert lol"),
|
||||
),
|
||||
/*final success = await login(
|
||||
_usernameController.text,
|
||||
_passwordController.text,
|
||||
);
|
||||
|
||||
if (success) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Login fehlgeschlagen!")),
|
||||
);
|
||||
}*/
|
||||
);
|
||||
},
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ExpansionTile(
|
||||
title: const Text('Theme'),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: themeMap.entries.map((entry) {
|
||||
final 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(
|
||||
const SnackBar(
|
||||
content: Text('jooong lass das, hier ist nichts'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
persistentFooterButtons: provider.tag != null
|
||||
persistentFooterButtons: mediaState.tag != null
|
||||
? [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('tag: '),
|
||||
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: mediaProvider.crossAxisCount == 0
|
||||
? (MediaQuery.of(context).size.width / 110)
|
||||
.clamp(3, 5)
|
||||
.toInt()
|
||||
: mediaProvider.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 test = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DetailView(initialItemId: item.id),
|
||||
),
|
||||
);
|
||||
if (test) {
|
||||
scrollToTop();
|
||||
}
|
||||
},
|
||||
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 {
|
||||
context.push('/${item.id}', extra: true);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
CachedNetworkImage(
|
||||
imageUrl: item.thumbnailUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => const SizedBox.shrink(),
|
||||
errorWidget: (context, url, error) =>
|
||||
const Icon(Icons.error),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Icon(
|
||||
Icons.square,
|
||||
color: switch (item.mode) {
|
||||
1 => Colors.green,
|
||||
2 => Colors.red,
|
||||
_ => Colors.yellow,
|
||||
},
|
||||
size: 15.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
94
lib/utils/ParseDeepLink.dart
Normal 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,*/
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
@ -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
@ -1 +0,0 @@
|
||||
flutter/ephemeral
|