Compare commits

...

46 Commits

Author SHA1 Message Date
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
e945844151 v1.1.10+40
All checks were successful
Flutter Schmutter / build (push) Successful in 3m56s
- download button lel
2025-06-09 19:08:23 +02:00
74eb6e3d26 readme & license 2025-06-09 15:42:13 +02:00
9755066d1e full retard renaming 2025-06-09 15:04:03 +02:00
671b3cfbe0 v1.1.9+39
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
2025-06-09 14:02:59 +02:00
93fb3536ee v1.1.8+38
All checks were successful
Flutter Schmutter / build (push) Successful in 3m48s
- blah
2025-06-08 19:40:06 +02:00
346e447d5e v1.1.7+37
All checks were successful
Flutter Schmutter / build (push) Successful in 3m44s
- worst update eu west
2025-06-08 17:16:10 +02:00
f7777821fd iiiiiicon 2025-06-08 10:33:05 +02:00
ffbde73300 v1.1.6+36
All checks were successful
Flutter Schmutter / build (push) Successful in 3m34s
- new theme: p1nk
- optimizations
2025-06-07 23:07:31 +02:00
836a0886e2 Icon Schmicon
All checks were successful
Flutter Schmutter / build (push) Successful in 3m33s
2025-06-07 20:41:06 +02:00
1cd10b3809 v1.1.5+35
- overlay buttons
- encrypted storage
- downloadbutton (wip)
2025-06-07 20:32:24 +02:00
43c42ac0d5 v1.1.4+34
All checks were successful
Flutter Schmutter / build (push) Successful in 3m27s
2025-06-07 16:52:30 +02:00
bf4e0fa493 v1.1.3+33
All checks were successful
Flutter Schmutter / build (push) Successful in 3m38s
2025-06-07 16:30:49 +02:00
27476fbc1d mute schmute 2025-06-07 12:28:24 +02:00
8e9f0eb1b8 v1.1.1+32
All checks were successful
Flutter Schmutter / build (push) Successful in 6m13s
2025-06-06 19:26:53 +02:00
f083fc8e8f ic_launcher_round 2025-06-06 18:32:59 +02:00
9a716018fc v1.1.1+31
All checks were successful
Flutter Schmutter / build (push) Successful in 3m28s
- fix share
- logo
2025-06-06 14:03:06 +02:00
f1eb52518b v1.1.0+30
All checks were successful
Flutter Schmutter / build (push) Successful in 3m27s
2025-06-06 12:58:21 +02:00
c7d996a402 v1.0.29+29
All checks were successful
Flutter Schmutter / build (push) Successful in 3m28s
2025-06-06 11:29:01 +02:00
ee93ef576b xd 2025-06-06 10:10:40 +02:00
78ff1953ad v1.0.28+28
All checks were successful
Flutter Schmutter / build (push) Successful in 3m30s
2025-06-06 08:43:50 +02:00
6fb4775043 v1.0.27+27 -.-
All checks were successful
Flutter Schmutter / build (push) Successful in 3m20s
2025-06-05 21:59:02 +02:00
0d9ed1e6c1 v1.0.26+26
All checks were successful
Flutter Schmutter / build (push) Successful in 3m20s
2025-06-05 19:53:25 +02:00
bf77ccf8e3 actionchip lel 2025-06-05 11:15:16 +02:00
ae5f395331 1.0.25+25
All checks were successful
Flutter Schmutter / build (push) Successful in 3m16s
- preload adjacent media
2025-06-05 08:41:48 +02:00
05484b342a revert -.-
All checks were successful
Flutter Schmutter / build (push) Successful in 3m12s
2025-06-04 13:38:39 +02:00
97d9259fab fml 2025-06-04 13:27:22 +02:00
b69a9843a7 fml 2025-06-04 13:26:32 +02:00
acacdef003 hmm
Some checks failed
Flutter Schmutter / build (push) Failing after 3m12s
2025-06-04 13:14:54 +02:00
3699e62efc oops
Some checks failed
Flutter Schmutter / build (push) Failing after 5s
2025-06-04 13:13:26 +02:00
666a02d293 sign test 2025-06-04 13:12:17 +02:00
28c4a17c43 v1.0.24+24
- tags lul
2025-06-04 12:35:09 +02:00
183 changed files with 2491 additions and 3776 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"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 f0ck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,16 +1,48 @@
# f0ckapp # fApp
![f0ck.me Logo](https://git.lat/f0ck/fApp/raw/branch/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png)
## Overview
A new Flutter project. fApp is the mobile application for the website [f0ck.me](https://f0ck.me). This app provides a user-friendly interface to access the content of the website and utilize its features conveniently from your mobile device.
## Getting Started ## Installation
This project is a starting point for a Flutter application. fApp is available in its own F-Droid repository.
A few resources to get you started if this is your first Flutter project: ### Installation Steps
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 1. Add the F-Droid repository to your F-Droid app:
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - Go to the settings in the F-Droid app.
- Select "Repositories" and add the URL `https://fdroid.flumm.io/fdroid/repo/`.
For help getting started with Flutter development, view the 2. Search for "f0ckapp" in the F-Droid app and install the app.
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference. ## Development
### Prerequisites
- Flutter SDK
- Dart SDK
- Android Studio or Visual Studio Code
### Setting Up the Project
1. Clone the repository:
```bash
git clone https://git.lat/f0ck/fApp.git
cd fApp
2. Install dependencies:
```flutter pub get```
3. Run the app:
```flutter run```
### Contributing
don't.
### License
This project is licensed under the MIT License. See the LICENSE file for more information.
### Contact
For questions or feedback, you can reach us at [contact@f0ck.me](mailto:contact@f0ck.me).

View File

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

View File

@ -1,10 +1,12 @@
<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.WAKE_LOCK"/>
<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">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -13,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
@ -27,12 +28,19 @@
<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>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="f0ck.me"/>
</intent-filter>
</activity> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and

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

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,27 +1,78 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:f0ckapp/screens/MediaGrid.dart'; import 'package:f0ckapp/screens/mediagrid_screen.dart';
import 'package:f0ckapp/screens/detailview_screen.dart';
import 'package:f0ckapp/screens/settings_screen.dart';
import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/providers/media_provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp(const F0ckApp()); await AppVersion.init();
runApp(ProviderScope(child: F0ckApp()));
} }
class F0ckApp extends StatelessWidget { class F0ckApp extends ConsumerWidget {
const F0ckApp({super.key}); const F0ckApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final ThemeData theme = ref.watch(themeNotifierProvider);
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: theme,
scaffoldBackgroundColor: const Color.fromARGB(255, 23, 23, 23), initialRoute: '/',
), routes: {
home: Scaffold( '/': (context) => const MediaGrid(),
body: MediaGrid(), '/settings': (context) => const SettingsPage(),
), },
onGenerateRoute: (RouteSettings settings) {
final String? name = settings.name;
if (name == null) {
return MaterialPageRoute(
builder: (_) =>
const Scaffold(body: Center(child: Text('Ungültiger Link'))),
settings: settings,
);
}
final RegExp regExp = RegExp(
r'^(?:/tag/(?<tag>[^/]+))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
);
final RegExpMatch? match = regExp.firstMatch(name);
if (match != null) {
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 int preloadOffset = 50;
if (itemId != null) {
return MaterialPageRoute(
builder: (context) => DetailView(initialItemId: itemId),
settings: settings,
);
}
return MaterialPageRoute(
builder: (context) => const MediaGrid(),
settings: settings,
);
}
return MaterialPageRoute(
builder: (context) =>
const Scaffold(body: Center(child: Text('Ungültiger Link'))),
settings: settings,
);
},
); );
} }
} }

View File

@ -34,6 +34,7 @@ class MediaItem {
String get thumbnailUrl => 'https://f0ck.me/t/$id.webp'; String get thumbnailUrl => 'https://f0ck.me/t/$id.webp';
String get mediaUrl => 'https://f0ck.me/b/$dest'; String get mediaUrl => 'https://f0ck.me/b/$dest';
String get coverUrl => 'https://f0ck.me/ca/$id.webp'; String get coverUrl => 'https://f0ck.me/ca/$id.webp';
String get postUrl => 'https://f0ck.me/$id';
} }
class Tag { class Tag {

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

@ -0,0 +1,163 @@
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 Set<int> existingIds = state.mediaItems
.map((item) => item.id)
.toSet();
final List<MediaItem> filteredItems = newItems
.where((item) => !existingIds.contains(item.id))
.toList();
if (filteredItems.isNotEmpty) {
final List<MediaItem> updated = List<MediaItem>.from(state.mediaItems)
..addAll(filteredItems);
state = state.replace(mediaItems: updated);
}
}
List<MediaItem> mergeMediaItems(
List<MediaItem> current,
List<MediaItem> incoming,
) {
final existingIds = current.map((item) => item.id).toSet();
final newItems = incoming
.where((item) => !existingIds.contains(item.id))
.toList();
return [...current, ...newItems];
}
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) {
state = state.replace(
mediaItems: mergeMediaItems(state.mediaItems, 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,288 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final Map<String, ThemeData> themeMap = {
'f0ck': f0ckTheme,
'P1nk': p1nkTheme, // done
'Orange': orangeTheme,
'Amoled': amoledTheme,
'Paper': paperTheme,
//'Iced': icedTheme,
'f0ck95': f0ck95Theme,
'f0ck95d': f0ck95dTheme,
};
class ThemeNotifier extends StateNotifier<ThemeData> {
final FlutterSecureStorage secureStorage;
ThemeNotifier({required this.secureStorage}) : super(f0ckTheme) {
_loadTheme();
}
Future<void> _loadTheme() async {
try {
String? savedThemeName = await secureStorage.read(key: 'theme');
if (savedThemeName != null && themeMap.containsKey(savedThemeName)) {
state = themeMap[savedThemeName]!;
}
} catch (error) {
debugPrint('Fehler beim Laden des Themes: $error');
state = f0ckTheme;
}
}
Future<void> updateTheme(String themeName) async {
try {
await secureStorage.write(key: 'theme', value: themeName);
state = themeMap[themeName] ?? f0ckTheme;
} catch (error) {
debugPrint('Fehler beim Aktualisieren des Themes: $error');
}
}
}
final themeNotifierProvider = StateNotifierProvider<ThemeNotifier, ThemeData>((
ref,
) {
return ThemeNotifier(
secureStorage: const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
),
);
});
final ThemeData f0ckTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF9FFF00),
scaffoldBackgroundColor: const Color(0xFF000000),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF9FFF00),
secondary: Color(0xFF262626),
surface: Color(0xFF232323),
onPrimary: Color(0xFF000000),
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2B2B2B),
foregroundColor: Color(0xFF9FFF00),
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF000000),
backgroundColor: const Color(0xFF9FFF00),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData p1nkTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF171717),
scaffoldBackgroundColor: const Color(0xFF171717),
appBarTheme: const AppBarTheme(
color: Color(0xFF2B2B2B),
foregroundColor: Color(0xFFFF00D0),
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Color(0xFFFFFFFF)),
bodyMedium: TextStyle(color: Color(0xFFFFFFFF)),
titleLarge: TextStyle(color: Color(0xFFFFFFFF)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFF000000),
backgroundColor: const Color(0xFFFF00D0),
),
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: Color(0xFFFF00D0),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all(const Color(0xFF424242)),
),
colorScheme: const ColorScheme.dark(
primary: Color(0xFF171717),
secondary: Color(0xFFFF00D0),
surface: Color(0xFF171717),
onPrimary: Color(0xFFFFFFFF),
onSecondary: Color(0xFF000000),
onSurface: Color(0xFFFFFFFF),
error: Color(0xFFA72828),
),
);
final ThemeData paperTheme = ThemeData(
brightness: Brightness.light,
primaryColor: const Color(0xFF000000),
scaffoldBackgroundColor: const Color(0xFFFFFFFF),
colorScheme: const ColorScheme.light(
primary: Color(0xFF000000),
secondary: Color(0xFF262626),
surface: Color(0xFFFFFFFF),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Color(0xFF000000),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFFFFFFFF),
foregroundColor: Color(0xFF000000),
elevation: 0,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Color(0xFF000000)),
bodyMedium: TextStyle(color: Color(0xFF000000)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: const Color(0xFFFFFFFF),
backgroundColor: const Color(0xFF000000),
),
),
);
final ThemeData orangeTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFF6F00),
scaffoldBackgroundColor: const Color(0xFF171717),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFF6F00),
secondary: Color(0xFF262626),
surface: Color(0xFF232323),
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF2B2B2B),
foregroundColor: Color(0xFFFF6F00),
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: const Color(0xFFFF6F00),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData amoledTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFFFFFF),
scaffoldBackgroundColor: const Color(0xFF000000),
canvasColor: const Color(0xFF000000),
cardColor: const Color(0xFF000000),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFFFFFF),
secondary: Color(0xFF1F1F1F),
surface: Color(0xFF000000),
onPrimary: Color(0xFF000000),
onSecondary: Colors.white,
onSurface: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF000000),
foregroundColor: Colors.white,
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: const Color(0xFFFFFFFF),
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF1D1C1C)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF000000)),
),
);
final ThemeData f0ck95Theme = ThemeData(
brightness: Brightness.light,
primaryColor: const Color(0xFFC0C0C0),
scaffoldBackgroundColor: const Color(0xFF008080),
colorScheme: const ColorScheme.light(
primary: Color(0xFFC0C0C0),
secondary: Color(0xFF808080),
surface: Color(0xFFC0C0C0),
onPrimary: Colors.black,
onSecondary: Colors.white,
),
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFFE0E0E0),
foregroundColor: Colors.black,
elevation: 4,
centerTitle: true
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.black),
bodyMedium: TextStyle(color: Colors.black),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: Colors.black,
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);
final ThemeData f0ck95dTheme = ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFFFFFF),
scaffoldBackgroundColor: const Color(0xFF0E0F0F),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFFFFFF),
secondary: Color(0xFFC0C0C0),
surface: Color(0xFF333131),
onPrimary: Colors.black,
onSecondary: Colors.white,
),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF0B0A0A),
foregroundColor: Colors.white,
elevation: 2,
),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.black,
backgroundColor: Colors.white,
),
),
scrollbarTheme: ScrollbarThemeData(
thumbColor: WidgetStateProperty.all<Color>(const Color(0xFF2B2B2B)),
trackColor: WidgetStateProperty.all<Color>(const Color(0xFF424242)),
),
);

View File

@ -1,169 +0,0 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/widgets/VideoWidget.dart';
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
import 'package:f0ckapp/utils/PageTransformer.dart';
class DetailView extends StatefulWidget {
final int initialItemId;
final List<MediaItem> mediaItems;
final String type;
final int mode;
final bool random;
const DetailView({
super.key,
required this.initialItemId,
required this.mediaItems,
required this.type,
required this.mode,
required this.random,
});
@override
State createState() => _DetailViewState();
}
class _DetailViewState extends State<DetailView> {
late PageController _pageController;
late List<MediaItem> mediaItems;
int currentItemId = 0;
bool isLoading = false;
@override
void initState() {
super.initState();
mediaItems = widget.mediaItems;
final initialIndex = mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
_pageController = PageController(initialPage: initialIndex);
currentItemId = mediaItems[initialIndex].id;
_pageController.addListener(_onPageScroll);
}
void _onPageScroll() {
final newIndex = _pageController.page?.round();
if (newIndex != null && newIndex < mediaItems.length) {
setState(() => currentItemId = mediaItems[newIndex].id);
}
if (_pageController.position.pixels >=
_pageController.position.maxScrollExtent - 100) {
_loadMoreMedia();
}
}
Future<void> _loadMoreMedia() async {
if (isLoading) return;
setState(() => isLoading = true);
try {
final newMedia = await fetchMedia(
older: mediaItems.last.id.toString(),
type: widget.type,
mode: widget.mode,
random: widget.random,
);
if (mounted && newMedia.isNotEmpty) {
setState(() => mediaItems.addAll(newMedia));
}
} catch (e) {
_showError("Fehler beim Laden weiterer Medien: $e");
} finally {
setState(() => isLoading = false);
}
}
Future<void> _refreshMediaItem() async {
try {
final updatedItem = await fetchMediaDetail(currentItemId);
if (mounted) {
final index = mediaItems.indexWhere((item) => item.id == currentItemId);
if (index != -1) {
setState(() => mediaItems[index] = updatedItem);
}
}
} catch (e) {
_showError("Fehler beim Aktualisieren des Items: $e");
}
}
void _showError(String message) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(message)));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF171717),
appBar: AppBar(
backgroundColor: const Color(0xFF2B2B2B),
foregroundColor: Colors.white,
title: Text('f0ck #$currentItemId (${widget.type})'),
centerTitle: true,
),
body: PageTransformer(
controller: _pageController,
pages: mediaItems.map((item) {
return Scaffold(
body: SafeArea(
child: SmartRefreshIndicator(
onRefresh: _refreshMediaItem,
child: _buildMediaItem(item),
),
),
);
}).toList(),
),
);
}
Widget _buildMediaItem(MediaItem item) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (item.mime.startsWith('image'))
CachedNetworkImage(
imageUrl: item.mediaUrl,
fit: BoxFit.contain,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
else
VideoWidget(details: item),
const SizedBox(height: 20),
Text(
item.mime,
style: const TextStyle(color: Colors.white, fontSize: 18),
),
const SizedBox(height: 10, width: double.infinity),
Wrap(
alignment: WrapAlignment.center,
spacing: 5.0,
children: item.tags.map((tag) {
return Chip(
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

@ -1,285 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/screens/DetailView.dart';
import 'dart:async';
class MediaGrid extends StatefulWidget {
const MediaGrid({super.key});
@override
State createState() => _MediaGridState();
}
class _MediaGridState extends State<MediaGrid> {
final ScrollController _scrollController = ScrollController();
final String _version = '1.0.23+23';
List<MediaItem> mediaItems = [];
bool isLoading = false;
Timer? _debounceTimer;
Completer<void>? _navigationCompleter;
int _crossAxisCount = 0;
String _selectedType = 'alles';
int _selectedMode = 0;
bool _random = false;
final List<String> _modes = ["sfw", "nsfw", "untagged", "all"];
@override
void initState() {
super.initState();
_loadMedia();
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
_debounceLoadMedia();
}
});
}
void _debounceLoadMedia() {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), _loadMedia);
}
int _calculateCrossAxisCount(BuildContext context) {
if (_crossAxisCount != 0) {
return _crossAxisCount;
}
double screenWidth = MediaQuery.of(context).size.width;
int columnCount = (screenWidth / 110).clamp(3, 5).toInt();
return columnCount;
}
Future<void> _loadMedia() async {
if (isLoading) return;
setState(() => isLoading = true);
try {
final newMedia = await fetchMedia(
older: mediaItems.isNotEmpty ? mediaItems.last.id.toString() : null,
type: _selectedType,
mode: _selectedMode,
random: _random,
);
if (mounted) {
setState(() => mediaItems.addAll(newMedia));
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der Medien: $e')),
);
}
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Future<void> _refreshMedia() async {
setState(() => isLoading = true);
try {
final freshMedia = await fetchMedia(
older: null,
type: _selectedType,
mode: _selectedMode,
random: _random,
);
if (mounted) {
setState(() {
mediaItems.clear();
mediaItems.addAll(freshMedia);
});
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Aktualisieren: $e')),
);
}
} finally {
if (mounted) setState(() => isLoading = false);
}
}
Future<void> _navigateToDetail(MediaItem item) async {
if (_navigationCompleter?.isCompleted == false) return;
_navigationCompleter = Completer();
try {
if (mounted) {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailView(
initialItemId: item.id,
mediaItems: mediaItems,
type: _selectedType,
mode: _selectedMode,
random: _random,
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der Details: $e')),
);
}
} finally {
_navigationCompleter?.complete();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
backgroundColor: const Color.fromARGB(255, 43, 43, 43),
foregroundColor: const Color.fromARGB(255, 255, 255, 255),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('f0ck v$_version'),
Checkbox(
value: _random,
onChanged: (bool? value) {
setState(() {
_random = !_random;
_refreshMedia();
});
},
)
]
)
),
bottomNavigationBar: BottomAppBar(
color: const Color.fromARGB(255, 43, 43, 43),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
DropdownButton<String>(
value: _selectedType,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: ["alles", "image", "video", "audio"].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedType = newValue;
_refreshMedia();
});
}
},
),
DropdownButton<String>(
value: _modes[_selectedMode],
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: _modes.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value, style: TextStyle(color: Colors.white)),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setState(() {
_selectedMode = _modes.indexOf(newValue);
_refreshMedia();
});
}
},
),
DropdownButton<int>(
value: _crossAxisCount,
dropdownColor: const Color.fromARGB(255, 43, 43, 43),
iconEnabledColor: Colors.white,
items: [0, 3, 4].map((int value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
value == 0 ? 'auto' : '$value Spalten',
style: TextStyle(color: Colors.white),
),
);
}).toList(),
onChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_crossAxisCount = newValue;
});
}
},
),
],
),
),
),
body: RefreshIndicator(
onRefresh: _refreshMedia,
child: GridView.builder(
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _calculateCrossAxisCount(context),
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemCount: mediaItems.length + (isLoading ? 1 : 0),
itemBuilder: (context, index) {
if (index >= mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaItems[index];
return InkWell(
onTap: () => _navigateToDetail(item),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
CachedNetworkImage(
imageUrl: item.thumbnailUrl,
fit: BoxFit.cover,
placeholder: (context, url) => SizedBox.shrink(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
Align(
alignment: FractionalOffset.bottomRight,
child: Icon(
Icons.square,
color: switch (item.mode) {
1 => Colors.green,
2 => Colors.red,
_ => Colors.yellow
},
size: 15.0
),
),
],
),
);
},
),
),
);
}
@override
void dispose() {
_scrollController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
}

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