Compare commits

..

21 Commits

Author SHA1 Message Date
ee2db04a36 v1.3.3+59
All checks were successful
Flutter Schmutter / build (push) Successful in 3m47s
2025-06-17 19:03:40 +02:00
089fe1f8df v1.3.2+58
All checks were successful
Flutter Schmutter / build (push) Successful in 3m50s
2025-06-16 19:10:00 +02:00
e9107a7f62 v1.3.1+57
All checks were successful
Flutter Schmutter / build (push) Successful in 3m52s
- oops xd
2025-06-16 15:12:28 +02:00
14081489cc v1.3.0+56
All checks were successful
Flutter Schmutter / build (push) Successful in 3m54s
2025-06-16 15:05:39 +02:00
2a500144f5 v1.2.1+55
All checks were successful
Flutter Schmutter / build (push) Successful in 3m42s
- fix deeplink
- add mute button
2025-06-13 15:01:48 +02:00
9655f15927 v1.2.0+54
All checks were successful
Flutter Schmutter / build (push) Successful in 3m37s
- screaming_possum.gif
2025-06-13 13:55:05 +02:00
dff9cda829 v1.1.23+53
All checks were successful
Flutter Schmutter / build (push) Successful in 3m40s
- bye go_router
2025-06-12 11:39:31 +02:00
16ebc51e77 v1.1.22+52
All checks were successful
Flutter Schmutter / build (push) Successful in 3m39s
2025-06-11 21:30:05 +02:00
7981436374 v1.1.21+51
All checks were successful
Flutter Schmutter / build (push) Successful in 3m40s
2025-06-11 20:45:37 +02:00
e38d2086b3 v1.1.20+50
All checks were successful
Flutter Schmutter / build (push) Successful in 3m39s
2025-06-11 18:56:55 +02:00
a4d50289c2 v1.1.19+49
All checks were successful
Flutter Schmutter / build (push) Successful in 3m36s
2025-06-11 14:53:26 +02:00
82fb23dbfd v1.1.18+48
All checks were successful
Flutter Schmutter / build (push) Successful in 3m36s
- fullscreen
2025-06-11 13:41:12 +02:00
13f957f016 test schmest
All checks were successful
Flutter Schmutter / build (push) Successful in 3m32s
2025-06-11 12:16:32 +02:00
707f14c5fb testbuild, rebranding
All checks were successful
Flutter Schmutter / build (push) Successful in 3m43s
2025-06-11 11:31:45 +02:00
493422e724 v1.1.15+45
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
- buildtest lol
2025-06-11 11:22:00 +02:00
3b95d128e1 fk gitea 2025-06-11 11:01:40 +02:00
57636c5de6 v1.1.14+44
All checks were successful
Flutter Schmutter / build (push) Successful in 3m32s
2025-06-11 10:52:15 +02:00
f75299f0d4 xd
All checks were successful
Flutter Schmutter / build (push) Successful in 3m53s
2025-06-10 18:53:52 +02:00
03c6431eca v1.1.13+43
- fk android
2025-06-10 18:53:07 +02:00
5876c809a5 v1.1.12+42
All checks were successful
Flutter Schmutter / build (push) Successful in 3m45s
- search schmearch
2025-06-10 11:07:00 +02:00
c35308fbc1 v1.1.11+41
All checks were successful
Flutter Schmutter / build (push) Successful in 3m57s
- fixed: duplicates on the frontpage
- new: search by tag
2025-06-10 08:39:55 +02:00
47 changed files with 2544 additions and 1291 deletions

View File

@ -3,7 +3,7 @@ name: Flutter Schmutter
on: on:
push: push:
tags: tags:
- 'v*' - '*'
jobs: jobs:
build: build:
@ -51,3 +51,11 @@ jobs:
files: |- files: |-
build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/app-release.apk
token: '${{secrets.RELEASE_TOKEN}}' token: '${{secrets.RELEASE_TOKEN}}'
- name: upload apk to f-droid server
run: |
BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
curl -X POST "https://flumm.io/pullfdroid.php" \
-F "token=${{ secrets.PULLER_TOKEN }}" \
-F "apk=@build/app/outputs/flutter-apk/app-release.apk" \
-F "build=$BUILD_NUMBER"

View File

@ -1,10 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application <application
android:label="f0ckapp" android:label="f0ck"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"> android:enableOnBackInvokedCallback="true">
@ -16,8 +15,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize">
android:requestLegacyExternalStorage="true">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
@ -30,19 +28,12 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="true"/>
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="f0ck.me"/> <data android:scheme="https" android:host="f0ck.me"/>
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="f0ck" android:host="com.f0ck.f0ckapp"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View File

@ -1,5 +1,69 @@
package com.f0ck.f0ckapp package com.f0ck.f0ckapp
import android.content.ContentValues
import android.content.Context
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.io.File
import java.io.FileInputStream
class MainActivity : FlutterActivity() class MainActivity : FlutterActivity() {
private val CHANNEL = "MediaShit"
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine): Unit {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call,
result ->
if (call.method == "saveFile") {
val filePath = call.argument<String>("filePath")
val fileName = call.argument<String>("fileName")
val subDir = call.argument<String?>("subDir")
if (filePath == null || fileName == null)
result.error("SAVE_FAILED", "file not found", null)
if (!saveFileUsingMediaStore(applicationContext, filePath!!, fileName!!, subDir))
result.error("COPY_FAILED", "Datei konnte nicht gespeichert werden", null)
result.success(true)
} else result.notImplemented()
}
}
private fun saveFileUsingMediaStore(
context: Context,
filePath: String,
fileName: String,
subDir: String?
): Boolean {
val srcFile = File(filePath)
if (!srcFile.exists()) return false
val values =
ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/" + (subDir ?: "f0ck"))
put(MediaStore.MediaColumns.IS_PENDING, 1)
}
val resolver = context.contentResolver
val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
val uri = resolver.insert(collection, values) ?: return false
resolver.openOutputStream(uri).use { out ->
FileInputStream(srcFile).use { input -> input.copyTo(out!!, 4096) }
}
values.clear()
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
resolver.update(uri, values, null, null)
return true
}
}

11
assets/i18n/de_DE.json Normal file
View File

@ -0,0 +1,11 @@
{
"settings_title": "Einstellungen",
"settings_language": "Sprache",
"settings_drawer_title": "Drawer per Geste öffnen",
"settings_drawer_subtitle": "Wähle, ob der Drawer mit einer Wischgeste geschlossen/geöffnet werden kann.",
"settings_numberofcolumns_title": "Spaltenanzahl",
"settings_numberofcolumns_columns": "@count Spalten",
"settings_pageanimation_title": "Seitenwechselanimation",
"settings_cache_title": "Cache leeren",
"settings_cache_clear_button": "Leeren"
}

11
assets/i18n/en_US.json Normal file
View File

@ -0,0 +1,11 @@
{
"settings_title": "Settings",
"settings_language": "Language",
"settings_drawer_title": "Open drawer with gesture",
"settings_drawer_subtitle": "Choose whether the drawer can be closed/opened with a swipe gesture.",
"settings_numberofcolumns_title": "Number of columns",
"settings_numberofcolumns_columns": "@count columns",
"settings_pageanimation_title": "Page change animation",
"settings_cache_title": "Clear Cache",
"settings_cache_clear_button": "Clear"
}

11
assets/i18n/fr_FR.json Normal file
View File

@ -0,0 +1,11 @@
{
"settings_title": "Paramètres",
"settings_language": "Langue",
"settings_drawer_title": "Ouvrir le tiroir avec un geste",
"settings_drawer_subtitle": "Choisissez si le tiroir peut être ouvert/fermé avec un geste de balayage.",
"settings_numberofcolumns_title": "Nombre de colonnes",
"settings_numberofcolumns_columns": "@count colonnes",
"settings_pageanimation_title": "Animation de changement de page",
"settings_cache_title": "Vider le cache",
"settings_cache_clear_button": "Vider"
}

11
assets/i18n/nl_NL.json Normal file
View File

@ -0,0 +1,11 @@
{
"settings_title": "Instellingen",
"settings_language": "Taal",
"settings_drawer_title": "Lade openen met een gebaar",
"settings_drawer_subtitle": "Kies of de lade geopend/gesloten kan worden met een veeggebaar.",
"settings_numberofcolumns_title": "Aantal kolommen",
"settings_numberofcolumns_columns": "@count kolommen",
"settings_pageanimation_title": "Pagina-overgangsanimatie",
"settings_cache_title": "Cache wissen",
"settings_cache_clear_button": "Wissen"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 B

View File

@ -0,0 +1,98 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:encrypt_shared_preferences/provider.dart';
import 'package:http/http.dart' as http;
import 'package:f0ckapp/controller/media_controller.dart';
class AuthController extends GetxController {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
final MediaController mediaController = Get.find<MediaController>();
RxnString token = RxnString();
RxnInt userId = RxnInt();
RxnString avatarUrl = RxnString();
RxnString username = RxnString();
@override
void onInit() {
super.onInit();
loadToken();
}
Future<void> loadToken() async {
token.value = await storage.getString('token');
if (token.value != null) {
await fetchUserInfo();
}
}
Future<void> saveToken(String newToken) async {
token.value = newToken;
await storage.setString('token', newToken);
await fetchUserInfo();
}
Future<void> logout() async {
if (token.value != null) {
try {
await http.post(
Uri.parse('https://api.f0ck.me/logout'),
headers: {
'Authorization': 'Bearer ${token.value}',
'Content-Type': 'application/json',
},
);
await mediaController.loadMediaItems();
mediaController.mediaItems.refresh();
} catch (e) {
//
}
}
token.value = null;
userId.value = null;
avatarUrl.value = null;
username.value = null;
await storage.remove('token');
}
Future<bool> login(String username, String password) async {
final http.Response response = await http.post(
Uri.parse('https://api.f0ck.me/login'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'username': username, 'password': password}),
);
if (response.statusCode == 200) {
final dynamic data = json.decode(response.body);
if (data['token'] != null) {
await saveToken(data['token']);
await mediaController.loadMediaItems();
mediaController.mediaItems.refresh();
return true;
}
}
return false;
}
Future<void> fetchUserInfo() async {
if (token.value == null) return;
final http.Response response = await http.get(
Uri.parse('https://api.f0ck.me/login/check'),
headers: {'Authorization': 'Bearer ${token.value}'},
);
if (response.statusCode == 200) {
final dynamic data = json.decode(response.body);
userId.value = data['userid'] != null
? int.parse(data['userid'].toString())
: null;
avatarUrl.value = data['avatar'] != null
? 'https://f0ck.me/t/${data['avatar']}.webp'
: null;
username.value = data['user'];
} else {
await logout();
}
}
}

View File

@ -0,0 +1,65 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:encrypt_shared_preferences/provider.dart';
class MyTranslations extends Translations {
static final MyTranslations instance = MyTranslations._internal();
MyTranslations._internal();
static final Map<String, Map<String, String>> _translations = {};
static Future<void> loadTranslations() async {
final locales = ['en_US', 'de_DE', 'fr_FR', 'nl_NL'];
for (final locale in locales) {
final String jsonString = await rootBundle.loadString(
'assets/i18n/$locale.json',
);
final Map<String, dynamic> jsonMap = json.decode(jsonString);
_translations[locale] = jsonMap.map(
(key, value) => MapEntry(key, value.toString()),
);
}
}
@override
Map<String, Map<String, String>> get keys => _translations;
}
class LocalizationController extends GetxController {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
Rx<Locale> currentLocale = const Locale('en', 'US').obs;
@override
void onInit() {
super.onInit();
loadLocale();
}
Future<void> loadLocale() async {
String? savedLocale = await storage.getString(
'locale',
defaultValue: 'en_US',
);
if (savedLocale != null && savedLocale.isNotEmpty) {
final List<String> parts = savedLocale.split('_');
currentLocale.value = parts.length == 2
? Locale(parts[0], parts[1])
: Locale(parts[0]);
Get.locale = currentLocale.value;
}
}
Future<void> changeLocale(Locale newLocale) async {
currentLocale.value = newLocale;
Get.updateLocale(newLocale);
await storage.setString(
'locale',
'${newLocale.languageCode}_${newLocale.countryCode}',
);
}
}

View File

@ -0,0 +1,112 @@
import 'package:encrypt_shared_preferences/provider.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/utils/animatedtransition.dart';
import 'package:f0ckapp/service/media_service.dart';
import 'package:f0ckapp/models/media_item.dart';
class MediaController extends GetxController {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
final RxList<MediaItem> mediaItems = <MediaItem>[].obs;
final RxBool isLoading = false.obs;
final RxString errorMessage = ''.obs;
final MediaService _mediaService = MediaService();
RxnString tag = RxnString();
RxInt type = 0.obs;
RxInt mode = 0.obs;
RxBool random = false.obs;
late RxBool muted = false.obs;
late RxInt crossAxisCount = 0.obs;
late RxBool drawerSwipeEnabled = true.obs;
final Rx<PageTransition> transitionType = PageTransition.opacity.obs;
MediaItem? selectedItem;
@override
void onInit() async {
super.onInit();
await loadSettings();
}
Future<void> loadSettings() async {
muted.value = await storage.getBoolean('muted') ?? false;
crossAxisCount.value = await storage.getInt('crossAxisCount') ?? 0;
drawerSwipeEnabled.value =
await storage.getBoolean('drawerSwipeEnabled') ?? true;
transitionType.value =
PageTransition.values[await storage.getInt('transitionType') ?? 0];
}
Future<void> saveSettings() async {
await storage.setBoolean('muted', muted.value);
await storage.setInt('crossAxisCount', crossAxisCount.value);
await storage.setBoolean('drawerSwipeEnabled', drawerSwipeEnabled.value);
await storage.setInt('transitionType', transitionType.value.index);
}
Future<void> setTag(String? newTag) async {
tag.value = newTag;
await loadMediaItems();
}
Future<void> setType(int newType) async {
type.value = newType;
await loadMediaItems();
}
Future<void> setMode(int newMode) async {
mode.value = newMode;
await loadMediaItems();
}
Future<void> toggleRandom() async {
random.value = !random.value;
await loadMediaItems();
}
Future<void> toggleMuted() async {
muted.value = !muted.value;
await saveSettings();
}
Future<void> setCrossAxisCount(int newCrossAxisCount) async {
crossAxisCount.value = newCrossAxisCount;
await saveSettings();
}
Future<void> setDrawerSwipeEnabled(bool newValue) async {
drawerSwipeEnabled.value = newValue;
await saveSettings();
}
Future<void> setTransitionType(PageTransition newType) async {
transitionType.value = newType;
await saveSettings();
}
Future<void> loadMediaItems({int? older, bool append = false}) async {
if (isLoading.value) return;
try {
isLoading.value = true;
final List<MediaItem> items = await _mediaService.fetchMediaItems(
type: type.value,
mode: mode.value,
random: random.value ? 1 : 0,
tag: tag.value,
older: older,
);
append ? mediaItems.addAll(items) : mediaItems.assignAll(items);
errorMessage.value = '';
} catch (e) {
errorMessage.value = 'Fehler beim Laden der Daten: ${e.toString()}';
Get.snackbar('Error', e.toString());
} finally {
isLoading.value = false;
}
}
}

View File

@ -1,57 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:encrypt_shared_preferences/provider.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( final ThemeData f0ckTheme = ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
@ -232,10 +182,11 @@ final ThemeData f0ck95Theme = ThemeData(
onPrimary: Colors.black, onPrimary: Colors.black,
onSecondary: Colors.white, onSecondary: Colors.white,
), ),
appBarTheme: const AppBarTheme( appBarTheme: AppBarTheme(
backgroundColor: Color(0xFFC0C0C0), backgroundColor: const Color(0xFFE0E0E0),
foregroundColor: Colors.black, foregroundColor: Colors.black,
elevation: 2, elevation: 4,
centerTitle: true,
), ),
textTheme: const TextTheme( textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black), bodyLarge: TextStyle(color: Colors.black),
@ -284,3 +235,55 @@ final ThemeData f0ck95dTheme = ThemeData(
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)), trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
), ),
); );
class ThemeController extends GetxController {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
final Rx<ThemeData> currentTheme = f0ckTheme.obs;
final Map<String, ThemeData> themeMap = {
'f0ck': f0ckTheme,
'P1nk': p1nkTheme,
'Orange': orangeTheme,
'Amoled': amoledTheme,
'Paper': paperTheme,
'f0ck95': f0ck95Theme,
'f0ck95d': f0ck95dTheme,
};
@override
void onInit() {
super.onInit();
_loadTheme();
}
Future<void> _loadTheme() async {
try {
final String? savedThemeName = await storage.getString('theme');
if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
currentTheme.value = themeMap[savedThemeName]!;
Get.changeTheme(currentTheme.value);
}
} catch (error) {
Get.snackbar('', 'Fehler beim Laden des Themes: $error');
currentTheme.value = f0ckTheme;
Get.changeTheme(f0ckTheme);
}
}
Future<void> updateTheme(String themeName) async {
try {
await storage.setString('theme', themeName);
if (themeMap.containsKey(themeName)) {
currentTheme.value = themeMap[themeName]!;
Get.changeTheme(currentTheme.value);
} else {
currentTheme.value = f0ckTheme;
Get.changeTheme(f0ckTheme);
}
} catch (error) {
Get.snackbar('', 'Fehler beim Aktualisieren des Themes: $error');
}
}
}

View File

@ -1,89 +1,57 @@
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:get/get.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:encrypt_shared_preferences/provider.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:f0ckapp/screens/mediagrid_screen.dart'; import 'package:f0ckapp/service/media_service.dart';
import 'package:f0ckapp/screens/detailview_screen.dart'; import 'package:f0ckapp/controller/localization_controller.dart';
import 'package:f0ckapp/utils/appversion_util.dart'; import 'package:f0ckapp/utils/appversion.dart';
import 'package:f0ckapp/providers/theme_provider.dart'; import 'package:f0ckapp/controller/theme_controller.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/screens/detail_view.dart';
import 'package:f0ckapp/screens/media_grid.dart';
import 'package:f0ckapp/controller/auth_controller.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await FlutterDownloader.initialize(); await EncryptedSharedPreferencesAsync.initialize('VokTnbAbemBUa2j9');
await MyTranslations.loadTranslations();
await AppVersion.init(); await AppVersion.init();
Get.put(MediaService());
Get.put(MediaController());
Get.put(AuthController());
LocalizationController localizationController = Get.put(LocalizationController());
final ThemeController themeController = Get.put(ThemeController());
runApp(ProviderScope(child: F0ckApp())); Get.addTranslations(MyTranslations.instance.keys);
} Get.locale = localizationController.currentLocale.value;
class F0ckApp extends ConsumerWidget { //Locale systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
F0ckApp({super.key});
final GoRouter _router = GoRouter( runApp(
initialLocation: '/', Obx(
routes: [ () => MaterialApp(
GoRoute( locale: Get.locale,
path: '/', navigatorKey: Get.key,
builder: (context, state) { theme: themeController.currentTheme.value,
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, WidgetRef ref) {
final theme = ref.watch(themeNotifierProvider);
return MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
routerConfig: _router, onGenerateRoute: (RouteSettings settings) {
theme: theme, final uri = Uri.parse(settings.name ?? '/');
);
if (uri.path == '/' || uri.pathSegments.isEmpty) {
return MaterialPageRoute(builder: (_) => MediaGrid());
} }
if (uri.pathSegments.length == 1) {
final int id = int.parse(uri.pathSegments.first);
return MaterialPageRoute(builder: (_) => DetailView(initialId: id));
}
return MaterialPageRoute(builder: (_) => MediaGrid());
},
),
),
);
} }

116
lib/models/media_item.dart Normal file
View File

@ -0,0 +1,116 @@
class MediaItem {
final int id;
final String mime;
final int size;
final int stamp;
final String dest;
final int mode;
final List<Tag> tags;
final List<Favorite>? favorites;
MediaItem({
required this.id,
required this.mime,
required this.size,
required this.stamp,
required this.dest,
required this.mode,
required this.tags,
required this.favorites,
});
MediaItem copyWith({
int? id,
String? mime,
int? size,
int? stamp,
String? dest,
int? mode,
List<Tag>? tags,
List<Favorite>? favorites,
}) {
return MediaItem(
id: id ?? this.id,
mime: mime ?? this.mime,
size: size ?? this.size,
stamp: stamp ?? this.stamp,
dest: dest ?? this.dest,
mode: mode ?? this.mode,
tags: tags ?? this.tags,
favorites: favorites ?? this.favorites,
);
}
factory MediaItem.fromJson(Map<String, dynamic> json) {
List<Tag> parsedTags = [];
if (json['tags'] is List) {
parsedTags = (json['tags'] as List<dynamic>)
.map((tagJson) => Tag.fromJson(tagJson as Map<String, dynamic>))
.toList();
} else {
parsedTags = [];
}
List<Favorite> parsedFavorites = [];
if (json['favorites'] is List) {
parsedFavorites = (json['favorites'] as List<dynamic>)
.map(
(favoritesJson) =>
Favorite.fromJson(favoritesJson as Map<String, dynamic>),
)
.toList();
} else {
parsedFavorites = [];
}
return MediaItem(
id: json['id'],
mime: json['mime'],
size: json['size'],
stamp: json['stamp'],
dest: json['dest'],
mode: json['mode'],
tags: parsedTags,
favorites: parsedFavorites,
);
}
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 {
final int id;
final String tag;
final String normalized;
Tag({required this.id, required this.tag, required this.normalized});
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
id: json['id'],
tag: json['tag'],
normalized: json['normalized'],
);
}
}
class Favorite {
final int userId;
final String user;
final int avatar;
Favorite({required this.userId, required this.user, required this.avatar});
factory Favorite.fromJson(Map<String, dynamic> json) {
return Favorite(
userId: json['user_id'],
user: json['user'],
avatar: json['avatar'],
);
}
String get avatarUrl => 'https://f0ck.me/t/$avatar.webp';
}

View File

@ -1,54 +0,0 @@
class MediaItem {
final int id;
final String mime;
final int size;
final int stamp;
final String dest;
final int mode;
final List<Tag> tags;
MediaItem({
required this.id,
required this.mime,
required this.size,
required this.stamp,
required this.dest,
required this.mode,
required this.tags,
});
factory MediaItem.fromJson(Map<String, dynamic> json) {
return MediaItem(
id: json['id'],
mime: json['mime'],
size: json['size'],
stamp: json['stamp'],
dest: json['dest'],
mode: json['mode'],
tags: (json['tags'] as List<dynamic>)
.map((tagJson) => Tag.fromJson(tagJson))
.toList(),
);
}
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 {
final int id;
final String tag;
final String normalized;
Tag({required this.id, required this.tag, required this.normalized});
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
id: json['id'],
tag: json['tag'],
normalized: json['normalized'],
);
}
}

View File

@ -0,0 +1,19 @@
class Suggestion {
final String tag;
final int tagged;
final double score;
Suggestion({
required this.tag,
required this.tagged,
required this.score,
});
factory Suggestion.fromJson(Map<String, dynamic> json) {
return Suggestion(
tag: json['tag'].toString(),
tagged: json['tagged'],
score: (json['score'] as num).toDouble(),
);
}
}

View File

@ -1,140 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/services/api_service.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
const _unsetTag = Object();
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;
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,
});
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();
}
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() {
state = state.replace(random: !state.random);
resetMedia();
}
void setTag(String? tag) {
state = state.replace(tag: tag);
resetMedia();
}
void setCrossAxisCount(int count) {
state = state.replace(crossAxisCount: count);
}
void resetMedia() {
state = state.replace(mediaItems: []);
loadMedia();
}
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: older,
type: mediaTypes[state.typeIndex],
mode: state.modeIndex,
random: state.random,
tag: state.tag,
);
if (newMedia.isNotEmpty) {
addMediaItems(newMedia);
}
} catch (e) {
print('Fehler beim Laden der Medien: $e');
} finally {
state = state.replace(isLoading: false);
}
}
void toggleMute() {
state = state.replace(muted: !state.muted);
_saveMutedState();
}
}
final mediaProvider = StateNotifierProvider<MediaNotifier, MediaState>(
(ref) => MediaNotifier(),
);

View File

@ -0,0 +1,293 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/service/media_service.dart';
import 'package:f0ckapp/utils/animatedtransition.dart';
import 'package:f0ckapp/utils/smartrefreshindicator.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/models/media_item.dart';
import 'package:f0ckapp/screens/media_grid.dart';
import 'package:f0ckapp/screens/fullscreen_screen.dart';
import 'package:f0ckapp/widgets/end_drawer.dart';
import 'package:f0ckapp/widgets/detailmediacontent.dart';
class DetailView extends StatefulWidget {
final int initialId;
const DetailView({super.key, required this.initialId});
@override
State<DetailView> createState() => _DetailViewState();
}
class _DetailViewState extends State<DetailView> {
final MediaController controller = Get.find<MediaController>();
MediaItem? item;
bool isLoading = false;
PageController? _pageController;
int _currentPage = 0;
@override
void initState() {
super.initState();
_setupInitialView();
ever(controller.drawerSwipeEnabled, (_) {
setState(() {});
});
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
Future<void> _setupInitialView() async {
bool itemExists = controller.mediaItems.any(
(media) => media.id == widget.initialId,
);
if (!itemExists) {
await _initializeDetail(widget.initialId);
}
_initializePageController();
}
void _initializePageController() {
final page = controller.mediaItems.indexWhere(
(media) => media.id == widget.initialId,
);
setState(() {
_currentPage = page < 0 ? 0 : page;
_pageController = PageController(initialPage: _currentPage)
..addListener(() {
setState(() => _currentPage = _pageController!.page!.round());
});
});
}
Future<void> _downloadMedia(MediaItem item) async {
final File file = await DefaultCacheManager().getSingleFile(item.mediaUrl);
final MethodChannel methodChannel = const MethodChannel('MediaShit');
bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
'filePath': file.path,
'fileName': item.dest,
});
success == true
? _showMsg('${item.dest} wurde in Downloads/fApp neigespeichert.')
: _showMsg('${item.dest} konnte nicht heruntergeladen werden.');
}
void _showMsg(String message) {
if (!mounted) return;
Get
..closeAllSnackbars()
..snackbar('hehe', message, snackPosition: SnackPosition.BOTTOM);
}
Future<void> _initializeDetail(int deepLinkId) async {
item = controller.mediaItems.firstWhereOrNull(
(element) => element.id == deepLinkId,
);
if (item == null) {
setState(() => isLoading = true);
await controller.loadMediaItems(older: deepLinkId + 50);
item = controller.mediaItems.firstWhereOrNull(
(element) => element.id == deepLinkId,
);
if (item == null) {
Get.offAll(() => const MediaGrid());
}
setState(() => isLoading = false);
}
}
@override
Widget build(BuildContext context) {
final MediaService mediaService = Get.find<MediaService>();
if (isLoading || controller.mediaItems.isEmpty || _pageController == null) {
return Scaffold(
appBar: AppBar(
title: const Text("f0ck"),
leading: Navigator.canPop(context)
? null
: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Get.offAll(() => const MediaGrid());
},
),
),
body: const Center(child: CircularProgressIndicator()),
);
}
final MediaItem currentItem = controller.mediaItems[_currentPage];
return Scaffold(
endDrawer: const EndDrawer(),
endDrawerEnableOpenDragGesture: controller.drawerSwipeEnabled.value,
persistentFooterButtons: controller.tag.value != null
? [
Center(
child: InputChip(
label: Text(controller.tag.value!),
onDeleted: () {
controller.setTag(null);
Get.offAll(() => const MediaGrid());
},
),
),
]
: null,
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: true,
pinned: true,
snap: true,
centerTitle: true,
title: Text('f0ck #${currentItem.id.toString()}'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.popUntil(
context,
(route) => route.settings.name == '/' || route.isFirst,
);
},
),
actions: [
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
Get.to(
FullScreenMediaView(item: currentItem),
fullscreenDialog: true,
);
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: () async {
await _downloadMedia(currentItem);
},
),
PopupMenuButton<String>(
onSelected: (value) async {
switch (value) {
case 'media':
File file = await DefaultCacheManager().getSingleFile(
currentItem.mediaUrl,
);
Uint8List bytes = await file.readAsBytes();
final params = ShareParams(
files: [
XFile.fromData(bytes, mimeType: currentItem.mime),
],
);
await SharePlus.instance.share(params);
break;
case 'direct_link':
await SharePlus.instance.share(
ShareParams(text: currentItem.mediaUrl),
);
break;
case 'post_link':
await SharePlus.instance.share(
ShareParams(text: currentItem.postUrl),
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'media',
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Als Datei'),
),
),
PopupMenuItem(
value: 'direct_link',
child: ListTile(
leading: const Icon(Icons.link),
title: const Text('Link zur Datei'),
),
),
PopupMenuItem(
value: 'post_link',
child: ListTile(
leading: const Icon(Icons.article),
title: const Text('Link zum f0ck'),
),
),
],
icon: const Icon(Icons.share),
),
Builder(
builder: (context) => IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
),
),
],
),
SliverFillRemaining(
child: PageView.builder(
controller: _pageController,
itemCount: controller.mediaItems.length,
itemBuilder: (context, index) {
final MediaItem pageItem = controller.mediaItems[index];
return AnimatedBuilder(
animation: _pageController!,
builder: (context, child) {
return buildAnimatedTransition(
context: context,
child: child!,
pageController: _pageController!,
index: index,
controller: controller,
);
},
child: SmartRefreshIndicator(
onRefresh: () async {
final MediaItem? refreshed = await mediaService.fetchItem(
pageItem.id,
);
if (refreshed != null) {
controller.mediaItems[index] = refreshed;
controller.mediaItems.refresh();
}
},
child: DetailMediaContent(
currentPage: _currentPage,
index: index,
onTagTap: (tag) {
if (tag == 'sfw' || tag == 'nsfw') return;
controller.setTag(tag);
Get.offAllNamed('/');
},
),
),
);
},
),
),
],
),
);
}
}

View File

@ -1,306 +0,0 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.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:permission_handler/permission_handler.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/widgets/video_widget.dart';
import 'package:f0ckapp/utils/smartrefreshindicator_util.dart';
import 'package:f0ckapp/utils/pagetransformer_util.dart';
import 'package:f0ckapp/providers/media_provider.dart';
class DetailView extends ConsumerStatefulWidget {
final int initialItemId;
const DetailView({super.key, required this.initialItemId});
@override
ConsumerState<DetailView> createState() => _DetailViewState();
}
class _DetailViewState extends ConsumerState<DetailView> {
PageController? _pageController;
bool isLoading = false;
int _currentIndex = 0;
@override
void initState() {
super.initState();
}
void _preloadAdjacentMedia(int index) async {
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);
}
}
}
}
Future<void> _loadMoreMedia() async {
if (isLoading) return;
setState(() => isLoading = true);
try {
await ref.read(mediaProvider.notifier).loadMedia();
} catch (e) {
_showError("Fehler beim Laden der Medien: $e");
} finally {
setState(() => isLoading = false);
}
}
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
Future<void> _downloadMedia() async {
final mediaState = ref.read(mediaProvider);
final currentItem = mediaState.mediaItems[_currentIndex];
if (Platform.isAndroid || Platform.isIOS) {
var status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
if (!status.isGranted) {
_showError("Speicherberechtigung wurde nicht erteilt.");
return;
}
}
}
String localPath;
if (Platform.isAndroid) {
final directory = await getExternalStorageDirectory();
localPath = "${directory!.path}/Download/fApp";
} else if (Platform.isIOS) {
final directory = await getApplicationDocumentsDirectory();
localPath = directory.path;
} else {
final directory = await getTemporaryDirectory();
localPath = directory.path;
}
final savedDir = Directory(localPath);
if (!await savedDir.exists()) {
await savedDir.create(recursive: true);
}
try {
await FlutterDownloader.enqueue(
url: currentItem.mediaUrl,
savedDir: localPath,
fileName: currentItem.mediaUrl.split('/').last,
showNotification: true,
openFileFromNotification: true,
);
if (mounted) {
_showError('Download gestartet: ${currentItem.mediaUrl}');
}
} catch (e) {
_showError('Download fehlgeschlagen: $e');
}
}
@override
void dispose() {
_pageController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider);
final int itemIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
print('itemIndex: ${itemIndex}; initial: ${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: 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('download ist wip');
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: _downloadMedia,
),
PopupMenuButton<String>(
onSelected: (value) async {
final item = mediaState.mediaItems[_currentIndex];
switch (value) {
case 'media':
File file = await DefaultCacheManager().getSingleFile(
item.mediaUrl,
);
Uint8List bytes = await file.readAsBytes();
final params = ShareParams(
files: [XFile.fromData(bytes, mimeType: item.mime)],
);
await SharePlus.instance.share(params);
break;
case 'direct_link':
await SharePlus.instance.share(
ShareParams(text: item.mediaUrl),
);
break;
case 'post_link':
await SharePlus.instance.share(
ShareParams(text: item.postUrl),
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'media',
child: ListTile(
leading: const Icon(Icons.image),
title: const Text('Als Datei'),
),
),
PopupMenuItem(
value: 'direct_link',
child: ListTile(
leading: const Icon(Icons.link),
title: const Text('Link zur Datei'),
),
),
PopupMenuItem(
value: 'post_link',
child: ListTile(
leading: const Icon(Icons.article),
title: const Text('Link zum f0ck'),
),
),
],
icon: const Icon(Icons.share),
),
],
),
body: Stack(
children: [
PageTransformer(
controller: _pageController!,
pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea(
child: SmartRefreshIndicator(
onRefresh: _loadMoreMedia,
child: _buildMediaItem(item, _currentIndex == itemIndex),
),
);
}).toList(),
),
],
),
persistentFooterButtons: mediaState.tag != null
? [
Center(
child: InputChip(
label: Text(mediaState.tag!),
onDeleted: () {
ref.read(mediaProvider.notifier).setTag(null);
context.go('/', extra: true);
},
),
),
]
: null,
);
}
Widget _buildMediaItem(MediaItem item, bool isActive) {
final mediaNotifier = ref.read(mediaProvider.notifier);
return SingleChildScrollView(
child: Column(
children: [
if (item.mime.startsWith('image'))
CachedNetworkImage(
imageUrl: item.mediaUrl,
fit: BoxFit.contain,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
else
VideoWidget(details: item, isActive: isActive),
const SizedBox(height: 10, width: double.infinity),
Wrap(
alignment: WrapAlignment.center,
spacing: 5.0,
children: item.tags.map((tag) {
return ActionChip(
onPressed: () {
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
setState(() {
mediaNotifier.setTag(tag.tag);
context.go('/', extra: true);
});
},
label: Text(tag.tag),
backgroundColor: switch (tag.id) {
1 => Colors.green,
2 => Colors.red,
_ => const Color(0xFF090909),
},
labelStyle: const TextStyle(color: Colors.white),
);
}).toList(),
),
const SizedBox(height: 20),
],
),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:f0ckapp/models/media_item.dart';
import 'package:f0ckapp/widgets/video_widget.dart';
class FullScreenMediaView extends StatefulWidget {
final MediaItem item;
const FullScreenMediaView({super.key, required this.item});
@override
State createState() => _FullScreenMediaViewState();
}
class _FullScreenMediaViewState extends State<FullScreenMediaView> {
@override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
@override
void dispose() {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: widget.item.mime.startsWith('image')
? InteractiveViewer(
minScale: 1.0,
maxScale: 7.0,
child: CachedNetworkImage(
imageUrl: widget.item.mediaUrl,
fit: BoxFit.contain,
placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
)
: SizedBox.expand(
child: VideoWidget(
details: widget.item,
isActive: true,
fullScreen: true,
),
),
),
SafeArea(
child: Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/controller/auth_controller.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<StatefulWidget> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final AuthController authController = Get.find();
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
void _showMsg(String message, {String title = ''}) {
Get
..closeAllSnackbars()
..snackbar(message, title, snackPosition: SnackPosition.BOTTOM);
}
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(floating: false, pinned: true, title: Text('Login')),
SliverList(
delegate: SliverChildListDelegate([
ListTile(
title: Text('Benutzername'),
subtitle: TextField(controller: usernameController),
),
ListTile(
title: Text('Passwort'),
subtitle: TextField(
controller: passwordController,
obscureText: true,
),
),
ElevatedButton(
onPressed: () async {
final success = await authController.login(
usernameController.text,
passwordController.text,
);
if (!success) {
_showMsg('Login fehlgeschlagen!');
}
},
child: Text('Login'),
),
]),
),
],
),
);
}
}

156
lib/screens/media_grid.dart Normal file
View File

@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/utils/customsearchdelegate.dart';
import 'package:f0ckapp/widgets/filter_bar.dart';
import 'package:f0ckapp/widgets/end_drawer.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/widgets/media_tile.dart';
class MediaGrid extends StatefulWidget {
const MediaGrid({super.key});
@override
State<MediaGrid> createState() => _MediaGridState();
}
class _MediaGridState extends State<MediaGrid> {
final MediaController controller = Get.find<MediaController>();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
controller.loadMediaItems();
ever(controller.drawerSwipeEnabled, (_) {
setState(() {});
});
_scrollController.addListener(() {
if (_scrollController.position.extentAfter < 200 &&
!controller.isLoading.value) {
controller.loadMediaItems(
older: controller.mediaItems.isNotEmpty
? controller.mediaItems.last.id
: null,
append: true,
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
endDrawer: EndDrawer(),
endDrawerEnableOpenDragGesture: controller.drawerSwipeEnabled.value,
body: RefreshIndicator(
edgeOffset: 100,
onRefresh: () async {
await controller.loadMediaItems();
},
child: CustomScrollView(
controller: _scrollController,
slivers: [
SliverAppBar(
floating: true,
snap: true,
title: GestureDetector(
child: Row(
children: [
Image.asset(
'assets/images/f0ck_small.webp',
fit: BoxFit.fitHeight,
),
const SizedBox(width: 10),
const Text('fApp', style: TextStyle(fontSize: 24)),
],
),
onTap: () {
controller.setTag(null);
},
),
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () async {
await showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
},
),
Obx(
() => IconButton(
icon: Icon(
controller.random.value
? Icons.shuffle_on_outlined
: Icons.shuffle,
),
onPressed: () async {
await controller.toggleRandom();
},
),
),
Builder(
builder: (context) {
return IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
);
},
),
],
),
SliverPadding(
padding: EdgeInsets.zero,
sliver: Obx(
() => SliverGrid(
delegate: SliverChildBuilderDelegate((context, index) {
return MediaTile(item: controller.mediaItems[index]);
}, childCount: controller.mediaItems.length),
gridDelegate: controller.crossAxisCount.value == 0
? const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 1,
)
: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: controller.crossAxisCount.value,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
childAspectRatio: 1,
),
),
),
),
],
),
),
persistentFooterButtons: [
Obx(() {
if (controller.tag.value != null) {
return Center(
child: InputChip(
label: Text(controller.tag.value!),
onDeleted: () async {
await controller.setTag(null);
Get.offAllNamed('/');
},
),
);
} else {
return SizedBox.shrink();
}
}),
],
bottomNavigationBar: FilterBar(scrollController: _scrollController),
);
}
}

View File

@ -1,315 +0,0 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
class MediaGrid extends ConsumerStatefulWidget {
const MediaGrid({super.key});
@override
ConsumerState<MediaGrid> createState() => _MediaGridState();
}
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();
Future.microtask(() {
ref.read(mediaProvider.notifier).loadMedia();
});
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
ref.read(mediaProvider.notifier).loadMedia();
}
});
}
@override
void dispose() {
_scrollController.dispose();
_usernameController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
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(
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
),
onPressed: () {
mediaNotifier.toggleRandom();
_scrollController.jumpTo(0);
},
),
IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState?.openEndDrawer();
},
),
],
),
bottomNavigationBar: BottomAppBar(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Text('type: '),
DropdownButton<String>(
value: mediaTypes[mediaState.typeIndex],
isDense: true,
items: mediaTypes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setType(newValue);
_scrollController.jumpTo(0);
}
},
),
const Text('mode: '),
DropdownButton<String>(
value: mediaModes[mediaState.modeIndex],
isDense: true,
items: mediaModes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
mediaNotifier.setMode(mediaModes.indexOf(newValue));
_scrollController.jumpTo(0);
}
},
),
],
),
),
endDrawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/menu.webp'),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
child: null,
),
ExpansionTile(
title: const Text('Login'),
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
readOnly: true,
controller: _usernameController,
decoration: const InputDecoration(
labelText: 'Benutzername',
),
),
const SizedBox(height: 10),
TextField(
readOnly: true,
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Passwort',
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () async {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("noch nicht implementiert lol"),
),
/*final success = await login(
_usernameController.text,
_passwordController.text,
);
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login fehlgeschlagen!")),
);
}*/
);
},
child: const Text('Login'),
),
],
),
),
],
),
ExpansionTile(
title: const Text('Theme'),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: themeMap.entries.map((entry) {
final 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: mediaState.tag != null
? [
Center(
child: InputChip(
label: Text(mediaState.tag!),
onDeleted: () {
mediaNotifier.setTag(null);
_scrollController.jumpTo(0);
},
),
),
]
: null,
body: RefreshIndicator(
onRefresh: () async {
mediaNotifier.resetMedia();
_scrollController.jumpTo(0);
},
child: GridView.builder(
key: const PageStorageKey('mediaGrid'),
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _calculateCrossAxisCount(
context,
mediaState.crossAxisCount,
),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount:
mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= mediaState.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaState.mediaItems[index];
return InkWell(
onTap: () async {
context.push('/${item.id}', extra: true);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
CachedNetworkImage(
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => const SizedBox.shrink(),
errorWidget: (context, url, error) =>
const Icon(Icons.error),
),
Align(
alignment: Alignment.bottomRight,
child: Icon(
Icons.square,
color: switch (item.mode) {
1 => Colors.green,
2 => Colors.red,
_ => Colors.yellow,
},
size: 15.0,
),
),
],
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/controller/localization_controller.dart';
import 'package:f0ckapp/utils/animatedtransition.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<StatefulWidget> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final MediaController controller = Get.find();
final LocalizationController localizationController = Get.find();
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
floating: false,
pinned: true,
title: Text('settings_title'.tr),
),
SliverList(
delegate: SliverChildListDelegate([
ListTile(
title: Text('settings_numberofcolumns_title'.tr),
trailing: Obx(
() => DropdownButton<int>(
value: controller.crossAxisCount.value,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: [0, 3, 4, 5].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
value == 0
? 'auto'
: 'settings_numberofcolumns_columns'.trParams({
'count': value.toString(),
}),
),
);
}).toList(),
onChanged: (int? newValue) async {
if (newValue != null) {
await controller.setCrossAxisCount(newValue);
setState(() {});
}
},
),
),
),
const Divider(),
ListTile(
title: Text('settings_pageanimation_title'.tr),
trailing: Obx(
() => DropdownButton<PageTransition>(
value: controller.transitionType.value,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: PageTransition.values.map((PageTransition type) {
String label;
switch (type) {
case PageTransition.opacity:
label = 'Opacity';
break;
case PageTransition.scale:
label = 'Scale';
break;
case PageTransition.slide:
label = 'Slide';
break;
case PageTransition.rotate:
label = 'Rotate';
break;
case PageTransition.flip:
label = 'Flip';
break;
}
return DropdownMenuItem<PageTransition>(
value: type,
child: Text(label),
);
}).toList(),
onChanged: (PageTransition? newValue) async {
if (newValue != null) {
await controller.setTransitionType(newValue);
setState(() {});
}
},
),
),
),
const Divider(),
SwitchListTile(
title: Text('settings_drawer_title'.tr),
subtitle: Text('settings_drawer_subtitle'.tr),
value: controller.drawerSwipeEnabled.value,
onChanged: (bool value) async {
await controller.setDrawerSwipeEnabled(value);
setState(() {});
},
),
const Divider(),
ListTile(
title: Text('settings_language'.tr),
trailing: Obx(
() => DropdownButton<Locale>(
value: localizationController.currentLocale.value,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: const [
DropdownMenuItem<Locale>(
value: Locale('en', 'US'),
child: Text('English'),
),
DropdownMenuItem<Locale>(
value: Locale('de', 'DE'),
child: Text('Deutsch'),
),
DropdownMenuItem<Locale>(
value: Locale('fr', 'FR'),
child: Text('Français'),
),
DropdownMenuItem<Locale>(
value: Locale('nl', 'NL'),
child: Text('Nederlands'),
),
],
onChanged: (Locale? newLocale) async {
if (newLocale != null) {
await localizationController.changeLocale(newLocale);
}
},
),
),
),
const Divider(),
ListTile(
title: Text('settings_cache_title'.tr),
trailing: ElevatedButton(
onPressed: () async {
await DefaultCacheManager().emptyCache();
if (!mounted) return;
Get.snackbar('', 'der Cache wurde geleert.');
},
child: Text('settings_cache_clear_button'.tr),
),
),
]),
),
],
),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:encrypt_shared_preferences/provider.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/models/media_item.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
class MediaService extends GetConnect {
final EncryptedSharedPreferencesAsync storage =
EncryptedSharedPreferencesAsync.getInstance();
Future<List<MediaItem>> fetchMediaItems({
required int type,
required int mode,
required int random,
String? tag,
int? older,
}) async {
final String? token = await storage.getString('token');
final Map<String, String> headers = token != null
? {'Authorization': 'Bearer $token'}
: {};
final queryParameters = {
'type': type.toString(),
'mode': mode.toString(),
'random': random.toString(),
if (tag != null) 'tag': tag,
if (older != null) 'older': older.toString(),
};
try {
final Response<dynamic> response = await get(
'https://api.f0ck.me/items/get',
query: queryParameters,
headers: headers,
);
if (response.status.code == 200 && response.body is List) {
final data = response.body as List<dynamic>;
return data.map((json) => MediaItem.fromJson(json)).toList();
} else {
return Future.error('Fehler beim Laden der Daten: ${response.body}');
}
} catch (e) {
return Future.error('Netzwerkfehler: ${e.toString()}');
}
}
Future<List<Favorite>?> toggleFavorite(
MediaItem item,
bool isFavorite,
) async {
final String? token = await storage.getString('token');
if (token == null) return null;
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
try {
Response response;
if (!isFavorite) {
response = await put(
'https://api.f0ck.me/favorites/${item.id}',
null,
headers: headers,
);
} else {
response = await delete(
'https://api.f0ck.me/favorites/${item.id}',
headers: headers,
);
}
if (response.status.code == 200 && response.body is List) {
return (response.body as List)
.map((json) => Favorite.fromJson(json))
.toList();
} else {
return null;
}
} catch (e) {
return null;
}
}
Future<MediaItem?> fetchItem(int itemId) async {
final String? token = await storage.getString('token');
final Map<String, String> headers = token != null
? {'Authorization': 'Bearer $token'}
: {};
try {
final Response<dynamic> response = await get(
'https://api.f0ck.me/item/$itemId',
headers: headers,
);
if (response.status.code == 200 && response.body is Map) {
return MediaItem.fromJson(response.body);
}
return null;
} catch (e) {
return null;
}
}
}

View File

@ -1,69 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
final storage = FlutterSecureStorage();
Future<List<MediaItem>> fetchMedia({
int? older,
String? type,
int? mode,
bool? random,
String? tag,
}) async {
final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace(
queryParameters: {
'type': type ?? 'image',
'mode': (mode ?? 0).toString(),
'random': (random! ? 1 : 0).toString(),
if (tag != null) 'tag': tag,
if (older != null) 'older': older.toString(),
},
);
final response = await http.get(url);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((item) => MediaItem.fromJson(item)).toList();
} else {
throw Exception('Fehler beim Abrufen der Medien: ${response.statusCode}');
}
}
Future<MediaItem> fetchMediaDetail(int itemId) async {
final Uri url = Uri.parse('https://api.f0ck.me/item/${itemId.toString()}');
final response = await http.get(url);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
return MediaItem.fromJson(jsonResponse);
} else {
throw Exception(
'Fehler beim Abrufen der Media-Details: ${response.statusCode}',
);
}
}
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,50 @@
import 'package:flutter/material.dart';
import 'package:f0ckapp/controller/media_controller.dart';
enum PageTransition { opacity, scale, slide, rotate, flip }
Widget buildAnimatedTransition({
required BuildContext context,
required Widget child,
required PageController pageController,
required int index,
required MediaController controller,
}) {
final double value = pageController.position.haveDimensions
? pageController.page! - index
: 0;
switch (controller.transitionType.value) {
case PageTransition.opacity:
return Opacity(
opacity: Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)),
child: Transform(transform: Matrix4.identity(), child: child),
);
case PageTransition.scale:
return Transform.scale(
scale:
0.8 +
Curves.easeOut.transform(1 - value.abs().clamp(0.0, 1.0)) * 0.2,
child: child,
);
case PageTransition.slide:
return Transform.translate(
offset: Offset(300 * value.abs(), 0),
child: child,
);
case PageTransition.rotate:
return Opacity(
opacity: (1 - value.abs()).clamp(0.0, 1.0),
child: Transform.rotate(angle: value.abs() * 0.5, child: child),
);
case PageTransition.flip:
return Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateY(value.abs()),
alignment: Alignment.center,
child: child,
);
}
}

12
lib/utils/appversion.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:flutter/services.dart';
class AppVersion {
static late String version;
static Future<void> init() async {
final String yaml = await rootBundle.loadString('pubspec.yaml');
final RegExpMatch? match = RegExp(r'^version:\s*(.*)$', multiLine: true).firstMatch(yaml);
final String? v = match?.group(1)!.replaceAll('"', '').replaceAll("'", '').trim();
version = v ?? "";
}
}

View File

@ -1,10 +0,0 @@
import 'package:package_info_plus/package_info_plus.dart';
class AppVersion {
static String version = "";
static Future<void> init() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
version = '${packageInfo.version}+${packageInfo.buildNumber}';
}
}

View File

@ -0,0 +1,185 @@
import 'dart:async';
import 'dart:convert';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
import 'package:f0ckapp/models/suggestion_model.dart';
class CustomSearchDelegate extends SearchDelegate<String> {
final MediaController controller = Get.find<MediaController>();
Timer? _debounceTimer;
List<Suggestion>? _suggestions;
bool _isLoading = false;
String? _error;
String _lastFetchedQuery = "";
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
_clearResults();
showSuggestions(context);
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
_debounceTimer?.cancel();
close(context, 'null');
},
);
}
@override
Widget buildResults(BuildContext context) {
return Center(child: Text('Suchergebnisse für: "$query"'));
}
Future<List<Suggestion>> fetchSuggestions(String query) async {
final Uri uri = Uri.parse('https://api.f0ck.me/search/?q=$query');
try {
final http.Response response = await http
.get(uri)
.timeout(const Duration(seconds: 5));
if (response.statusCode == 200) {
final dynamic decoded = jsonDecode(response.body);
if (decoded is List) {
final suggestions = decoded
.map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
.toList();
suggestions.sort((a, b) => b.score.compareTo(a.score));
return suggestions;
} else {
throw Exception('Unerwartetes Format: Es wurde eine Liste erwartet.');
}
} else if (response.statusCode == 400) {
final dynamic error = jsonDecode(response.body);
final String message = error is Map<String, dynamic>
? error['detail']?.toString() ?? 'Unbekannter Fehler.'
: 'Unbekannter Fehler.';
throw Exception('Client-Fehler 400: $message');
} else {
throw Exception(
'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
);
}
} on TimeoutException {
throw Exception('Anfrage an die API hat zu lange gedauert.');
} catch (e) {
throw Exception('Fehler bei der Verarbeitung der Anfrage: $e');
}
}
@override
Widget buildSuggestions(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setState) {
if (query.isEmpty) {
_debounceTimer?.cancel();
return Container(
padding: const EdgeInsets.all(16.0),
child: const Text(''),
);
}
if (query != _lastFetchedQuery) {
_debounceTimer?.cancel();
_isLoading = true;
_error = null;
_suggestions = null;
_debounceTimer = Timer(Duration(milliseconds: 500), () async {
try {
final List<Suggestion> results = await fetchSuggestions(query);
_lastFetchedQuery = query;
setState(() {
_suggestions = results;
_isLoading = false;
});
} catch (e) {
_lastFetchedQuery = query;
setState(() {
_error = e.toString();
_suggestions = [];
_isLoading = false;
});
}
});
return Center(child: _buildLoadingIndicator());
}
if (_isLoading) {
return Center(child: _buildLoadingIndicator());
}
if (_error != null) {
return Center(child: Text("Fehler: $_error"));
}
if (_suggestions == null || _suggestions!.isEmpty) {
return Center(child: const Text("Keine Ergebnisse gefunden."));
}
return ListView.builder(
itemCount: _suggestions!.length,
itemBuilder: (BuildContext context, int index) {
final Suggestion suggestion = _suggestions![index];
return ListTile(
title: Text(suggestion.tag),
subtitle: Text(
'Getaggt: ${suggestion.tagged}x • Score: ${suggestion.score.toStringAsFixed(2)}',
style: TextStyle(fontSize: 12),
),
onTap: () async {
await controller.setTag(suggestion.tag);
close(context, suggestion.tag);
},
);
},
);
},
);
}
Widget _buildLoadingIndicator() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(strokeWidth: 3.0),
const SizedBox(height: 12),
const Text(
'Vorschläge werden geladen...',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
);
}
void _clearResults() {
_debounceTimer?.cancel();
_suggestions = null;
_isLoading = false;
_error = null;
_lastFetchedQuery = "";
}
@override
void close(BuildContext context, String result) {
_debounceTimer?.cancel();
super.close(context, result);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
class PageTransformer extends StatelessWidget {
final List<Widget> pages;
final PageController controller;
const PageTransformer({
super.key,
required this.pages,
required this.controller,
});
@override
Widget build(BuildContext context) {
return PageView.builder(
controller: controller,
itemCount: pages.length,
itemBuilder: (context, index) {
return _buildPage(pages[index], index);
},
);
}
Widget _buildPage(Widget page, int index) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
double value = 1.0;
if (controller.position.haveDimensions) {
value = controller.page! - index;
value = (1 - (value.abs() * 0.5)).clamp(0.0, 1.0);
}
return Transform(
transform: Matrix4.identity()..scale(value, value),
alignment: Alignment.center,
child: child,
);
},
child: page,
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:f0ckapp/models/media_item.dart';
class ActionTag extends StatelessWidget {
final Tag tag;
final void Function(String tag) onTagTap;
const ActionTag(this.tag, this.onTagTap, {super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => onTagTap(tag.tag),
child:
[
'german',
'dutch',
'ukraine',
'russia',
'belgium',
].contains(tag.tag)
? Stack(
alignment: Alignment.center,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 1),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/tags/${tag.tag}.webp',
height: 27,
width: 60,
repeat: ImageRepeat.repeat,
fit: BoxFit.fill,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 9,
vertical: 6,
),
child: Text(
tag.tag,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 1),
color: switch (tag.id) {
1 => Colors.green,
2 => Colors.red,
_ => const Color(0xFF090909),
},
),
child: Text(
tag.tag,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
);
}
}

View File

@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/service/media_service.dart';
import 'package:f0ckapp/controller/auth_controller.dart';
import 'package:f0ckapp/widgets/actiontag.dart';
import 'package:f0ckapp/widgets/favorite_avatars.dart';
import 'package:f0ckapp/widgets/video_widget.dart';
import 'package:f0ckapp/models/media_item.dart';
class DetailMediaContent extends StatelessWidget {
final int currentPage;
final int index;
final void Function(String tag) onTagTap;
const DetailMediaContent({
super.key,
required this.currentPage,
required this.index,
required this.onTagTap,
});
@override
Widget build(BuildContext context) {
final MediaService mediaService = Get.find();
final MediaController controller = Get.find();
final AuthController authController = Get.find();
return SafeArea(
top: false,
child: SingleChildScrollView(
child: Obx(() {
final MediaItem currentItem = controller.mediaItems[index];
final bool isFavorite =
currentItem.favorites?.any(
(f) => f.userId == authController.userId.value,
) ??
false;
return Column(
children: [
_buildMedia(currentItem, index == currentPage),
const SizedBox(height: 10, width: double.infinity),
_buildTags(currentItem),
if (currentItem.favorites != null &&
authController.token.value != null) ...[
const SizedBox(height: 20),
_buildFavoritesRow(context, currentItem, isFavorite, () async {
final List<Favorite>? newFavorites = await mediaService
.toggleFavorite(currentItem, isFavorite);
if (newFavorites != null) {
controller.mediaItems[index] = currentItem.copyWith(
favorites: newFavorites,
);
controller.mediaItems.refresh();
}
}),
],
const SizedBox(height: 20),
],
);
}),
),
);
}
Widget _buildMedia(MediaItem item, bool isActive) {
if (item.mime.startsWith('image')) {
return CachedNetworkImage(
imageUrl: item.mediaUrl,
fit: BoxFit.contain,
placeholder: (context, url) =>
const Center(child: CircularProgressIndicator()),
errorWidget: (context, url, error) =>
const Center(child: Icon(Icons.error)),
);
} else {
return VideoWidget(details: item, isActive: isActive);
}
}
Widget _buildTags(MediaItem item) {
return Wrap(
alignment: WrapAlignment.center,
spacing: 5.0,
children: item.tags
.map<Widget>((Tag tag) => ActionTag(tag, onTagTap))
.toList(),
);
}
Widget _buildFavoritesRow(
BuildContext context,
MediaItem item,
bool isFavorite,
VoidCallback onFavoritePressed,
) {
return Row(
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: FavoriteAvatars(
favorites: item.favorites ?? [],
brightness: Theme.of(context).brightness,
),
),
),
IconButton(
icon: isFavorite
? const Icon(Icons.favorite)
: const Icon(Icons.favorite_outline),
color: Colors.red,
onPressed: onFavoritePressed,
),
],
);
}
}

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

@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/screens/login_screen.dart';
import 'package:f0ckapp/controller/auth_controller.dart';
import 'package:f0ckapp/screens/settings_screen.dart';
import 'package:f0ckapp/controller/theme_controller.dart';
import 'package:f0ckapp/utils/appversion.dart';
class EndDrawer extends StatelessWidget {
const EndDrawer({super.key});
void _showMsg(String message, {String title = ''}) {
Get
..closeAllSnackbars()
..snackbar(message, title, snackPosition: SnackPosition.BOTTOM);
}
@override
Widget build(BuildContext context) {
final ThemeController themeController = Get.find();
final AuthController authController = Get.find();
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
Obx(() {
if (authController.token.value != null &&
authController.avatarUrl.value != null) {
return DrawerHeader(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(authController.avatarUrl.value!),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
child: null,
);
} else {
return DrawerHeader(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/menu.webp'),
fit: BoxFit.cover,
alignment: Alignment.topCenter,
),
),
child: null,
);
}
}),
Obx(() {
if (authController.token.value != null) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
if (authController.username.value != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Hamlo ${authController.username.value!}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
ElevatedButton(
onPressed: () async {
await authController.logout();
_showMsg('Erfolgreich ausgeloggt.');
},
child: const Text('Logout'),
),
],
),
);
} else {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Du bist nicht eingeloggt.',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
Get.bottomSheet(LoginPage(), isDismissible: false);
},
child: const Text('Login'),
),
],
),
);
}
}),
ExpansionTile(
title: const Text('Theme'),
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Obx(() {
return Column(
children: themeController.themeMap.entries.map((entry) {
final String themeName = entry.key;
final ThemeData themeData = entry.value;
final bool isSelected =
themeController.currentTheme.value == themeData;
return ListTile(
title: Text(themeName),
selected: isSelected,
selectedTileColor: Colors.blue.withValues(alpha: 0.2),
onTap: () async {
await themeController.updateTheme(themeName);
},
);
}).toList(),
);
}),
),
],
),
ListTile(
title: const Text('Settings'),
onTap: () {
Navigator.pop(context);
Get.bottomSheet(SettingsPage());
},
),
ListTile(
title: Text('v${AppVersion.version}'),
onTap: () {
Navigator.pop(context);
_showMsg('jooong lass das, hier ist nichts');
},
),
],
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
class FavoriteAvatars extends StatelessWidget {
final List favorites;
final Brightness brightness;
const FavoriteAvatars({
super.key,
required this.favorites,
required this.brightness,
});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...favorites.map((favorite) {
return Container(
height: 32,
width: 32,
margin: const EdgeInsets.only(right: 5.0),
decoration: BoxDecoration(
border: Border.all(
color: brightness == Brightness.dark
? Colors.white
: Colors.black,
width: 1.0,
),
),
child: CachedNetworkImage(
imageUrl: favorite.avatarUrl,
fit: BoxFit.cover,
),
);
}),
],
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/service/media_service.dart';
class FilterBar extends StatelessWidget {
final ScrollController scrollController;
const FilterBar({
super.key,
required this.scrollController,
});
@override
Widget build(BuildContext context) {
final MediaController c = Get.find<MediaController>();
return BottomAppBar(
height: 50,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const Text('type: '),
Obx(() => DropdownButton<String>(
value: mediaTypes[c.type.value],
isDense: true,
items: mediaTypes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
c.setType(mediaTypes.indexOf(newValue));
scrollController.jumpTo(0);
}
},
)),
const Text('mode: '),
Obx(() => DropdownButton<String>(
value: mediaModes[c.mode.value],
isDense: true,
items: mediaModes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
c.setMode(mediaModes.indexOf(newValue));
scrollController.jumpTo(0);
}
},
)),
],
),
);
}
}

View File

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

View File

@ -2,25 +2,32 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/models/mediaitem_model.dart'; import 'package:f0ckapp/controller/media_controller.dart';
import 'package:f0ckapp/models/media_item.dart';
import 'package:f0ckapp/widgets/videooverlay_widget.dart'; import 'package:f0ckapp/widgets/videooverlay_widget.dart';
import 'package:f0ckapp/providers/media_provider.dart';
class VideoWidget extends ConsumerStatefulWidget { class VideoWidget extends StatefulWidget {
final MediaItem details; final MediaItem details;
final bool isActive; final bool isActive;
final bool fullScreen;
const VideoWidget({super.key, required this.details, required this.isActive}); const VideoWidget({
super.key,
required this.details,
required this.isActive,
this.fullScreen = false,
});
@override @override
ConsumerState<VideoWidget> createState() => _VideoWidgetState(); State<VideoWidget> createState() => _VideoWidgetState();
} }
class _VideoWidgetState extends ConsumerState<VideoWidget> { class _VideoWidgetState extends State<VideoWidget> {
final MediaController controller = Get.find<MediaController>();
late CachedVideoPlayerPlusController _controller; late CachedVideoPlayerPlusController _controller;
bool _showControls = false; bool _showControls = false;
Timer? _hideControlsTimer; Timer? _hideControlsTimer;
@ -45,8 +52,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
} }
_controller.setLooping(true); _controller.setLooping(true);
final muted = ref.read(mediaProvider).muted; _controller.setVolume(controller.muted.value ? 0.0 : 1.0);
_controller.setVolume(muted ? 0.0 : 1.0);
} }
@override @override
@ -82,7 +88,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final muted = ref.watch(mediaProvider).muted; final bool muted = controller.muted.value;
if (_controller.value.isInitialized && if (_controller.value.isInitialized &&
_controller.value.volume != (muted ? 0.0 : 1.0)) { _controller.value.volume != (muted ? 0.0 : 1.0)) {
_controller.setVolume(muted ? 0.0 : 1.0); _controller.setVolume(muted ? 0.0 : 1.0);
@ -90,6 +96,50 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
bool isAudio = widget.details.mime.startsWith('audio'); bool isAudio = widget.details.mime.startsWith('audio');
if (widget.fullScreen) {
return Stack(
children: [
Center(
child: AspectRatio(
aspectRatio: _controller.value.isInitialized
? _controller.value.aspectRatio
: 9 / 16,
child: GestureDetector(
onTap: _onTap,
child: isAudio
? CachedNetworkImage(
imageUrl: widget.details.coverUrl,
fit: BoxFit.cover,
placeholder: (context, url) =>
const CircularProgressIndicator(),
errorWidget: (context, url, error) => Image.asset(
'assets/images/music.webp',
fit: BoxFit.contain,
width: double.infinity,
),
)
: _controller.value.isInitialized
? CachedVideoPlayerPlus(_controller)
: const Center(child: CircularProgressIndicator()),
),
),
),
if (_controller.value.isInitialized && _showControls)
Positioned.fill(
child: GestureDetector(
onTap: _onTap,
child: Container(
color: Colors.black.withValues(alpha: 0.5),
child: VideoControlsOverlay(
controller: _controller,
button: () => _onTap(ctrlButton: true),
),
),
),
),
],
);
} else {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -138,4 +188,5 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
], ],
); );
} }
}
} }

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart'; import 'package:cached_video_player_plus/cached_video_player_plus.dart';
import 'package:get/get.dart';
import 'package:f0ckapp/providers/media_provider.dart'; import 'package:f0ckapp/controller/media_controller.dart';
class VideoControlsOverlay extends ConsumerWidget { class VideoControlsOverlay extends StatelessWidget {
final CachedVideoPlayerPlusController controller; final CachedVideoPlayerPlusController controller;
final VoidCallback button; final VoidCallback button;
@ -16,9 +16,8 @@ class VideoControlsOverlay extends ConsumerWidget {
}); });
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider); final MediaController c = Get.find<MediaController>();
final mediaNotifier = ref.read(mediaProvider.notifier);
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
@ -26,15 +25,17 @@ class VideoControlsOverlay extends ConsumerWidget {
Positioned( Positioned(
right: 12, right: 12,
bottom: 12, bottom: 12,
child: _ControlButton( child: Obx(
mediaState.muted ? Icons.volume_off : Icons.volume_up, () => _ControlButton(
() { c.muted.value ? Icons.volume_off : Icons.volume_up,
() async {
button(); button();
mediaNotifier.toggleMute(); await c.toggleMuted();
}, },
size: 16, size: 16,
), ),
), ),
),
Center( Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -1,6 +1,22 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "0511d6be23b007e95105ae023db599aea731df604608978dada7f9faf2637623"
url: "https://pub.dev"
source: hosted
version: "1.6.4"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -73,6 +89,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -97,14 +121,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
cupertino_icons: devtools_shared:
dependency: "direct main" dependency: transitive
description: description:
name: cupertino_icons name: devtools_shared
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 sha256: "659e2d65aa5ef5c3551163811c5c6fa1b973b3df80d8cac6f618035edcdc1096"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "11.2.1"
dtd:
dependency: transitive
description:
name: dtd
sha256: "14a0360d898ded87c3d99591fc386b8a6ea5d432927bee709b22130cd25b993a"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
encrypt:
dependency: transitive
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
encrypt_shared_preferences:
dependency: "direct main"
description:
name: encrypt_shared_preferences
sha256: ab8a957db7ae645c8b0341e8aee85c1cd046a5cb9a0529459ea417ebd6040ba2
url: "https://pub.dev"
source: hosted
version: "0.9.9"
extension_discovery:
dependency: transitive
description:
name: extension_discovery
sha256: de1fce715ab013cdfb00befc3bdf0914bea5e409c3a567b7f8f144bc061611a7
url: "https://pub.dev"
source: hosted
version: "2.1.0"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -150,14 +206,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
flutter_downloader:
dependency: "direct main"
description:
name: flutter_downloader
sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -166,62 +214,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -233,7 +225,7 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
get: get:
dependency: transitive dependency: "direct main"
description: description:
name: get name: get
sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425
@ -248,14 +240,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175
url: "https://pub.dev"
source: hosted
version: "15.1.3"
html: html:
dependency: transitive dependency: transitive
description: description:
@ -288,6 +272,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
sha256: "246b321532f0e8e2ba474b4d757eaa558ae4fdd0688fdbc1e1ca9705f9b8ca0e"
url: "https://pub.dev"
source: hosted
version: "3.0.3"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -368,22 +360,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.0" version: "2.1.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -393,7 +369,7 @@ packages:
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
dependency: "direct main" dependency: transitive
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@ -440,54 +416,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
url: "https://pub.dev"
source: hosted
version: "12.0.0+1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -504,14 +432,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
riverpod: pointycastle:
dependency: transitive dependency: transitive
description: description:
name: riverpod name: pointycastle
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.1" version: "3.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -536,6 +472,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
shared_preferences:
dependency: transitive
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -597,6 +597,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.0"
sse:
dependency: transitive
description:
name: sse
sha256: fcc97470240bb37377f298e2bd816f09fd7216c07928641c0560719f50603643
url: "https://pub.dev"
source: hosted
version: "4.1.8"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -605,14 +613,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.1" version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -661,6 +661,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
unified_analytics:
dependency: transitive
description:
name: unified_analytics
sha256: c8abdcad84b55b78f860358aae90077b8f54f98169a75e16d97796a1b3c95590
url: "https://pub.dev"
source: hosted
version: "8.0.1"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
@ -757,6 +765,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:
@ -773,6 +805,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yaml_edit:
dependency: transitive
description:
name: yaml_edit
sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5
url: "https://pub.dev"
source: hosted
version: "2.2.2"
sdks: sdks:
dart: ">=3.9.0-100.2.beta <4.0.0" dart: ">=3.9.0-100.2.beta <4.0.0"
flutter: ">=3.29.0" flutter: ">=3.29.0"

View File

@ -1,5 +1,5 @@
name: f0ckapp name: f0ckapp
description: "A new Flutter project." description: "f0ck schm0ck"
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.10+40 version: 1.3.3+59
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta
@ -32,19 +32,11 @@ dependencies:
sdk: flutter sdk: flutter
http: ^1.4.0 http: ^1.4.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
cached_network_image: ^3.4.1 cached_network_image: ^3.4.1
cached_video_player_plus: ^3.0.3 cached_video_player_plus: ^3.0.3
package_info_plus: ^8.3.0
share_plus: ^11.0.0 share_plus: ^11.0.0
flutter_secure_storage: ^9.2.4 encrypt_shared_preferences: ^0.9.9
flutter_riverpod: ^2.6.1 get: ^4.7.2
go_router: ^15.1.3
flutter_downloader: ^1.12.0
permission_handler: ^12.0.0+1
path_provider: ^2.1.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -71,6 +63,9 @@ flutter:
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
assets: assets:
- assets/images/ - assets/images/
- assets/images/tags/
- assets/i18n/
- pubspec.yaml
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg

View File

@ -8,12 +8,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:f0ckapp/main.dart'; //import 'package:f0ckapp/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async { testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
await tester.pumpWidget(const F0ckApp()); //await tester.pumpWidget(F0ckApp());
// Verify that our counter starts at 0. // Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);