Compare commits
19 Commits
v1.1.8+38
...
v1.1.23+53
Author | SHA1 | Date | |
---|---|---|---|
dff9cda829 | |||
16ebc51e77 | |||
7981436374 | |||
e38d2086b3 | |||
a4d50289c2 | |||
82fb23dbfd | |||
13f957f016 | |||
707f14c5fb | |||
493422e724 | |||
3b95d128e1 | |||
57636c5de6 | |||
f75299f0d4 | |||
03c6431eca | |||
5876c809a5 | |||
c35308fbc1 | |||
e945844151 | |||
74eb6e3d26 | |||
9755066d1e | |||
671b3cfbe0 |
@ -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
21
LICENSE
Normal 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.
|
52
README.md
52
README.md
@ -1,16 +1,48 @@
|
|||||||
# f0ckapp
|
# fApp
|
||||||
|

|
||||||
|
## 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).
|
||||||
|
@ -1,9 +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"/>
|
||||||
<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">
|
||||||
@ -15,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
|
||||||
@ -29,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="false"/>
|
|
||||||
<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 -->
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,34 +1,76 @@
|
|||||||
import 'package:app_links/app_links.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:f0ckapp/screens/MediaGrid.dart';
|
|
||||||
import 'package:f0ckapp/utils/AppVersion.dart';
|
|
||||||
import 'package:f0ckapp/providers/ThemeProvider.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.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]);
|
||||||
await AppVersion.init();
|
await AppVersion.init();
|
||||||
final Uri? initialUri = await AppLinks().getInitialLink();
|
|
||||||
|
|
||||||
runApp(ProviderScope(child: F0ckApp(initialUri: initialUri)));
|
runApp(ProviderScope(child: F0ckApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
class F0ckApp extends ConsumerWidget {
|
class F0ckApp extends ConsumerWidget {
|
||||||
final Uri? initialUri;
|
const F0ckApp({super.key});
|
||||||
const F0ckApp({super.key, this.initialUri});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Consumer(
|
final ThemeData theme = ref.watch(themeNotifierProvider);
|
||||||
builder: (context, ref, _) {
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: ref.watch(themeNotifierProvider),
|
theme: theme,
|
||||||
home: MediaGrid(initialUri: initialUri),
|
initialRoute: '/',
|
||||||
|
routes: {
|
||||||
|
'/': (context) => const 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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
19
lib/models/suggestion_model.dart
Normal file
19
lib/models/suggestion_model.dart
Normal 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
import 'package:f0ckapp/models/MediaItem.dart';
|
import 'package:f0ckapp/models/mediaitem_model.dart';
|
||||||
import 'package:f0ckapp/services/Api.dart';
|
import 'package:f0ckapp/services/api_service.dart';
|
||||||
|
|
||||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
||||||
@ -102,16 +102,32 @@ class MediaNotifier extends StateNotifier<MediaState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void addMediaItems(List<MediaItem> newItems) {
|
void addMediaItems(List<MediaItem> newItems) {
|
||||||
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
|
final Set<int> existingIds = state.mediaItems
|
||||||
state = state.replace(mediaItems: updated);
|
.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 {
|
Future<void> loadMedia({int? id}) async {
|
||||||
//if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
if (id != null) {
|
|
||||||
print('requested id: ${id.toString()}');
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.replace(isLoading: true);
|
state = state.replace(isLoading: true);
|
||||||
try {
|
try {
|
||||||
final older =
|
final older =
|
||||||
@ -123,8 +139,11 @@ class MediaNotifier extends StateNotifier<MediaState> {
|
|||||||
random: state.random,
|
random: state.random,
|
||||||
tag: state.tag,
|
tag: state.tag,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newMedia.isNotEmpty) {
|
if (newMedia.isNotEmpty) {
|
||||||
addMediaItems(newMedia);
|
state = state.replace(
|
||||||
|
mediaItems: mergeMediaItems(state.mediaItems, newMedia),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Fehler beim Laden der Medien: $e');
|
print('Fehler beim Laden der Medien: $e');
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
@ -231,10 +233,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),
|
@ -1,245 +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:share_plus/share_plus.dart';
|
|
||||||
|
|
||||||
import 'package:f0ckapp/models/MediaItem.dart';
|
|
||||||
import 'package:f0ckapp/widgets/VideoWidget.dart';
|
|
||||||
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
|
|
||||||
import 'package:f0ckapp/utils/PageTransformer.dart';
|
|
||||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
|
||||||
|
|
||||||
class DetailView extends ConsumerStatefulWidget {
|
|
||||||
final int initialItemId;
|
|
||||||
|
|
||||||
const DetailView({super.key, required this.initialItemId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<DetailView> createState() => _DetailViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DetailViewState extends ConsumerState<DetailView> {
|
|
||||||
late PageController _pageController;
|
|
||||||
bool isLoading = false;
|
|
||||||
int _currentIndex = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final mediaState = ref.read(mediaProvider);
|
|
||||||
final initialIndex = mediaState.mediaItems.indexWhere(
|
|
||||||
(item) => item.id == widget.initialItemId,
|
|
||||||
);
|
|
||||||
_pageController = PageController(initialPage: initialIndex);
|
|
||||||
_currentIndex = initialIndex;
|
|
||||||
|
|
||||||
_pageController.addListener(() {
|
|
||||||
setState(() => _currentIndex = _pageController.page?.round() ?? 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
_preloadAdjacentMedia(initialIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_pageController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final mediaState = ref.watch(mediaProvider);
|
|
||||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
|
||||||
|
|
||||||
if (mediaState.mediaItems.isEmpty) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
body: const Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
centerTitle: true,
|
|
||||||
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.fullscreen),
|
|
||||||
onPressed: () {
|
|
||||||
// wip
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.download),
|
|
||||||
onPressed: () {
|
|
||||||
// wip
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuButton<String>(
|
|
||||||
onSelected: (value) async {
|
|
||||||
final item = 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: () {
|
|
||||||
mediaNotifier.setTag(null);
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: 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);
|
|
||||||
Navigator.pop(context, 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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,345 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import 'package:f0ckapp/screens/DetailView.dart';
|
|
||||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
|
||||||
import 'package:f0ckapp/utils/AppVersion.dart';
|
|
||||||
import 'package:f0ckapp/utils/ParseDeepLink.dart';
|
|
||||||
import 'package:f0ckapp/providers/ThemeProvider.dart';
|
|
||||||
|
|
||||||
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
|
|
||||||
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
|
|
||||||
|
|
||||||
class MediaGrid extends ConsumerStatefulWidget {
|
|
||||||
final Uri? initialUri = null;
|
|
||||||
const MediaGrid({super.key, required initialUri});
|
|
||||||
|
|
||||||
@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();
|
|
||||||
|
|
||||||
final appLinks = AppLinks();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
appLinks.uriLinkStream.listen((Uri uri) async {
|
|
||||||
final parsedResult = parseDeepLink(uri);
|
|
||||||
if (parsedResult == null) return;
|
|
||||||
if (parsedResult['route'] != 'complex') return;
|
|
||||||
final params = parsedResult['params'] as Map<String, String>;
|
|
||||||
await handleComplexDeepLink(params, context, ref, _scrollController);
|
|
||||||
});
|
|
||||||
|
|
||||||
//print('initial: ${parseDeepLink(widget.initialUri)}');
|
|
||||||
Future.microtask(() async {
|
|
||||||
final initparsedResult = parseDeepLink(widget.initialUri);
|
|
||||||
if (initparsedResult == null) return;
|
|
||||||
if (initparsedResult['route'] != 'complex') return;
|
|
||||||
final initparams = initparsedResult['params'] as Map<String, String>;
|
|
||||||
await handleComplexDeepLink(initparams, context, ref, _scrollController);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
_usernameController.dispose();
|
|
||||||
_passwordController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final 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 {
|
|
||||||
bool? ret = await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => DetailView(initialItemId: item.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (ret != null && ret) {
|
|
||||||
_scrollController.jumpTo(0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: <Widget>[
|
|
||||||
CachedNetworkImage(
|
|
||||||
imageUrl: item.thumbnailUrl,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: (context, url) => const SizedBox.shrink(),
|
|
||||||
errorWidget: (context, url, error) =>
|
|
||||||
const Icon(Icons.error),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
child: Icon(
|
|
||||||
Icons.square,
|
|
||||||
color: switch (item.mode) {
|
|
||||||
1 => Colors.green,
|
|
||||||
2 => Colors.red,
|
|
||||||
_ => Colors.yellow,
|
|
||||||
},
|
|
||||||
size: 15.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
299
lib/screens/detailview_screen.dart
Normal file
299
lib/screens/detailview_screen.dart
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/screens/fullscreen_screen.dart';
|
||||||
|
import 'package:f0ckapp/widgets/end_drawer.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.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) {
|
||||||
|
_showMsg("Fehler beim Laden der Medien: $e");
|
||||||
|
} finally {
|
||||||
|
setState(() => isLoading = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMsg(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadMedia() async {
|
||||||
|
final MediaState mediaState = ref.read(mediaProvider);
|
||||||
|
final MediaItem currentItem = mediaState.mediaItems[_currentIndex];
|
||||||
|
final File file = await DefaultCacheManager().getSingleFile(
|
||||||
|
currentItem.mediaUrl,
|
||||||
|
);
|
||||||
|
final MethodChannel methodChannel = const MethodChannel('MediaShit');
|
||||||
|
|
||||||
|
bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
|
||||||
|
'filePath': file.path,
|
||||||
|
'fileName': currentItem.dest,
|
||||||
|
});
|
||||||
|
|
||||||
|
success == true
|
||||||
|
? _showMsg(
|
||||||
|
'${currentItem.dest} wurde in Downloads/fApp neigespeichert.',
|
||||||
|
)
|
||||||
|
: _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
|
final int itemIndex = mediaState.mediaItems.indexWhere(
|
||||||
|
(item) => item.id == widget.initialItemId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (itemIndex == -1) {
|
||||||
|
Future.microtask(() {
|
||||||
|
ref.read(mediaProvider.notifier).loadMedia(id: widget.initialItemId + 50);
|
||||||
|
});
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pageController == null) {
|
||||||
|
_pageController = PageController(initialPage: itemIndex);
|
||||||
|
_currentIndex = itemIndex;
|
||||||
|
_pageController!.addListener(() {
|
||||||
|
setState(() => _currentIndex = _pageController!.page?.round() ?? 0);
|
||||||
|
});
|
||||||
|
_preloadAdjacentMedia(itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
endDrawer: EndDrawer(ref: ref),
|
||||||
|
endDrawerEnableOpenDragGesture: false,
|
||||||
|
persistentFooterButtons: mediaState.tag != null
|
||||||
|
? [
|
||||||
|
Center(
|
||||||
|
child: InputChip(
|
||||||
|
label: Text(mediaState.tag!),
|
||||||
|
onDeleted: () {
|
||||||
|
ref.read(mediaProvider.notifier).setTag(null);
|
||||||
|
//context.push('/', extra: true);
|
||||||
|
Navigator.pushNamed(context, '/');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
centerTitle: true,
|
||||||
|
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushNamed(context, '/');
|
||||||
|
//context.canPop() ? context.pop() : context.go('/', extra: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.fullscreen),
|
||||||
|
onPressed: () {
|
||||||
|
final currentItem = mediaState.mediaItems[_currentIndex];
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => FullScreenMediaView(item: currentItem),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) => IconButton(
|
||||||
|
icon: const Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
Scaffold.of(context).openEndDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
sliver: SliverFillRemaining(
|
||||||
|
child: PageTransformer(
|
||||||
|
controller: _pageController!,
|
||||||
|
pages: mediaState.mediaItems.map((item) {
|
||||||
|
int pageIndex = mediaState.mediaItems.indexOf(item);
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: SmartRefreshIndicator(
|
||||||
|
onRefresh: _loadMoreMedia,
|
||||||
|
child: _buildMediaItem(item, _currentIndex == pageIndex),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMediaItem(MediaItem item, bool isActive) {
|
||||||
|
final MediaNotifier 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 Center(child: CircularProgressIndicator()),
|
||||||
|
errorWidget: (context, url, error) =>
|
||||||
|
const Center(child: 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);
|
||||||
|
Navigator.pushReplacementNamed(context, '/');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
74
lib/screens/fullscreen_screen.dart
Normal file
74
lib/screens/fullscreen_screen.dart
Normal 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/mediaitem_model.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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
162
lib/screens/mediagrid_screen.dart
Normal file
162
lib/screens/mediagrid_screen.dart
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
|
||||||
|
import 'package:f0ckapp/widgets/media_tile.dart';
|
||||||
|
import 'package:f0ckapp/widgets/filter_bar.dart';
|
||||||
|
import 'package:f0ckapp/widgets/end_drawer.dart';
|
||||||
|
|
||||||
|
class MediaGrid extends ConsumerStatefulWidget {
|
||||||
|
const MediaGrid({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<MediaGrid> createState() => _MediaGridState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@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();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
mediaNotifier.setTag(null);
|
||||||
|
_scrollController.jumpTo(0);
|
||||||
|
await mediaNotifier.loadMedia();
|
||||||
|
},
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: true,
|
||||||
|
snap: true,
|
||||||
|
title: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
mediaNotifier.setTag(null);
|
||||||
|
_scrollController.jumpTo(0);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/f0ck_small.webp',
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Text('fApp', style: TextStyle(fontSize: 24)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () async {
|
||||||
|
await showSearch(
|
||||||
|
context: context,
|
||||||
|
delegate: CustomSearchDelegate(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
mediaState.random
|
||||||
|
? Icons.shuffle_on_outlined
|
||||||
|
: Icons.shuffle,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
mediaNotifier.toggleRandom();
|
||||||
|
_scrollController.jumpTo(0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.menu),
|
||||||
|
onPressed: () {
|
||||||
|
Scaffold.of(context).openEndDrawer();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
sliver: SliverGrid(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (index >= mediaState.mediaItems.length) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
return MediaTile(item: mediaState.mediaItems[index]);
|
||||||
|
},
|
||||||
|
childCount:
|
||||||
|
mediaState.mediaItems.length +
|
||||||
|
(mediaState.isLoading ? 1 : 0),
|
||||||
|
),
|
||||||
|
gridDelegate: mediaState.crossAxisCount == 0
|
||||||
|
? const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
|
maxCrossAxisExtent: 150,
|
||||||
|
crossAxisSpacing: 5,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
childAspectRatio: 1,
|
||||||
|
)
|
||||||
|
: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: mediaState.crossAxisCount,
|
||||||
|
crossAxisSpacing: 5,
|
||||||
|
mainAxisSpacing: 5,
|
||||||
|
childAspectRatio: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: FilterBar(
|
||||||
|
mediaNotifier: mediaNotifier,
|
||||||
|
mediaState: mediaState,
|
||||||
|
scrollController: _scrollController,
|
||||||
|
),
|
||||||
|
endDrawer: EndDrawer(ref: ref),
|
||||||
|
endDrawerEnableOpenDragGesture: false,
|
||||||
|
persistentFooterButtons: mediaState.tag != null
|
||||||
|
? [
|
||||||
|
Center(
|
||||||
|
child: InputChip(
|
||||||
|
label: Text(mediaState.tag!),
|
||||||
|
onDeleted: () {
|
||||||
|
mediaNotifier.setTag(null);
|
||||||
|
_scrollController.jumpTo(0);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
lib/screens/settings_screen.dart
Normal file
106
lib/screens/settings_screen.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
import 'package:f0ckapp/widgets/end_drawer.dart';
|
||||||
|
|
||||||
|
class SettingsPage extends ConsumerStatefulWidget {
|
||||||
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||||
|
int _columns = 3;
|
||||||
|
bool _drawerSwipeEnabled = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
endDrawerEnableOpenDragGesture: _drawerSwipeEnabled,
|
||||||
|
endDrawer: EndDrawer(ref: ref),
|
||||||
|
body: CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppBar(
|
||||||
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
|
title: const Text('Settings'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.canPop(context) ? Navigator.pop(context) : Navigator.pushReplacementNamed(context, '/');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildListDelegate([
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Text(
|
||||||
|
"Anzahl der Spalten",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text("Spaltenanzahl"),
|
||||||
|
trailing: DropdownButton<int>(
|
||||||
|
value: mediaState.crossAxisCount,
|
||||||
|
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' : '$value Spalten'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (int? newValue) {
|
||||||
|
if (newValue != null) {
|
||||||
|
setState(() {
|
||||||
|
_columns = newValue;
|
||||||
|
mediaNotifier.setCrossAxisCount(newValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
SwitchListTile(
|
||||||
|
title: const Text("Drawer per Geste öffnen"),
|
||||||
|
subtitle: const Text(
|
||||||
|
"Wähle, ob der Drawer mit einer Wischgeste geschlossen/ geöffnet werden kann.",
|
||||||
|
),
|
||||||
|
value: _drawerSwipeEnabled,
|
||||||
|
onChanged: (bool value) {
|
||||||
|
setState(() {
|
||||||
|
_drawerSwipeEnabled = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text("Cache löschen"),
|
||||||
|
trailing: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await DefaultCacheManager().emptyCache();
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("Cache wurde geleert.")),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Text("Löschen"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.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;
|
|
||||||
}
|
|
||||||
}
|
|
111
lib/services/api_service.dart
Normal file
111
lib/services/api_service.dart
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
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';
|
||||||
|
import 'package:f0ckapp/models/suggestion_model.dart';
|
||||||
|
|
||||||
|
final FlutterSecureStorage storage = const FlutterSecureStorage(
|
||||||
|
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<List<MediaItem>> fetchMedia({
|
||||||
|
int? older,
|
||||||
|
String type = 'image',
|
||||||
|
int mode = 0,
|
||||||
|
bool random = false,
|
||||||
|
String? tag,
|
||||||
|
}) async {
|
||||||
|
final Uri url = Uri.parse('https://api.f0ck.me/items/get').replace(
|
||||||
|
queryParameters: {
|
||||||
|
'type': type,
|
||||||
|
'mode': mode.toString(),
|
||||||
|
'random': (random ? 1 : 0).toString(),
|
||||||
|
if (tag != null) 'tag': tag,
|
||||||
|
if (older != null) 'older': older.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final http.Response 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 http.Response 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<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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> login(String username, String password) async {
|
||||||
|
final Uri url = Uri.parse('https://api.f0ck.me/login');
|
||||||
|
|
||||||
|
final http.Response response = await http.post(
|
||||||
|
url,
|
||||||
|
body: {'username': username, 'password': password},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final dynamic data = jsonDecode(response.body);
|
||||||
|
final token = data['token'];
|
||||||
|
if (token != null) {
|
||||||
|
await storage.write(key: "token", value: token);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw Exception('Token nicht im Response enthalten.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('Login fehlgeschlagen: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
}
|
@ -1,94 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
|
||||||
import 'package:f0ckapp/screens/DetailView.dart';
|
|
||||||
|
|
||||||
Map<String, RegExp> routes = {
|
|
||||||
//'login': RegExp(r'^/login/?$'),
|
|
||||||
//'user': RegExp(r'^/user/(?<user>.*)$'),
|
|
||||||
'complex': RegExp(
|
|
||||||
r'^/?'
|
|
||||||
r'(?:tag/(?<tag>.+?))?'
|
|
||||||
r'(?:/user/(?<username>.+?)/(?<set>f0cks|favs))?'
|
|
||||||
r'(?:/(?<media>image|audio|video))?'
|
|
||||||
r'(?:/(?<id>\d+))?'
|
|
||||||
r'$',
|
|
||||||
),
|
|
||||||
//'random': RegExp(r'^/random$'),
|
|
||||||
//'search': RegExp(r'^/search/?$'),
|
|
||||||
};
|
|
||||||
|
|
||||||
Map<String, dynamic> parseDeepLink(Uri? uri) {
|
|
||||||
if (uri == null) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
String url = uri.toString().replaceAll("https://f0ck.me", "");
|
|
||||||
|
|
||||||
for (final MapEntry<String, RegExp> entry in routes.entries) {
|
|
||||||
final String routeName = entry.key;
|
|
||||||
final RegExp pattern = entry.value;
|
|
||||||
final RegExpMatch? match = pattern.firstMatch(url.toString());
|
|
||||||
if (match != null) {
|
|
||||||
Map<String, String> params = <String, String>{};
|
|
||||||
for (String name in match.groupNames) {
|
|
||||||
params[name] = match.namedGroup(name) ?? '';
|
|
||||||
}
|
|
||||||
return {'route': routeName, 'params': params};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> handleComplexDeepLink(
|
|
||||||
Map<String, String> params,
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
ScrollController scrollController,
|
|
||||||
) async {
|
|
||||||
final media = params['media'];
|
|
||||||
const validMediaTypes = {'audio', 'video', 'image'};
|
|
||||||
if (media != null && validMediaTypes.contains(media)) {
|
|
||||||
ref.read(mediaProvider.notifier).setType(media);
|
|
||||||
} else {
|
|
||||||
ref.read(mediaProvider.notifier).setType('alles');
|
|
||||||
}
|
|
||||||
ref.read(mediaProvider.notifier).setMode(0); // wip
|
|
||||||
ref.read(mediaProvider.notifier).setTag(null);
|
|
||||||
|
|
||||||
final idParam = params['id'];
|
|
||||||
if (idParam == null || idParam.isEmpty) return;
|
|
||||||
|
|
||||||
final int? id = int.tryParse(idParam);
|
|
||||||
if (id == null) return;
|
|
||||||
|
|
||||||
final mediaState = ref.read(mediaProvider);
|
|
||||||
final index = mediaState.mediaItems.indexWhere((item) => item.id == id);
|
|
||||||
if (index == -1) {
|
|
||||||
await ref.read(mediaProvider.notifier).loadMedia(id: id + 50);
|
|
||||||
|
|
||||||
final updatedState = ref.read(mediaProvider);
|
|
||||||
final updatedIndex = updatedState.mediaItems.indexWhere(
|
|
||||||
(item) => item.id == id,
|
|
||||||
);
|
|
||||||
print(updatedIndex.toString());
|
|
||||||
if (updatedIndex == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool? navigationResult = await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (_) => DetailView(initialItemId: id)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (navigationResult == true) {
|
|
||||||
scrollController.jumpTo(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
type: mediaTypes[state.typeIndex],
|
|
||||||
mode: state.modeIndex,
|
|
||||||
random: state.random,
|
|
||||||
tag: state.tag,*/
|
|
145
lib/utils/customsearchdelegate_util.dart
Normal file
145
lib/utils/customsearchdelegate_util.dart
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:f0ckapp/services/api_service.dart';
|
||||||
|
import 'package:f0ckapp/models/suggestion_model.dart';
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
|
||||||
|
class CustomSearchDelegate extends SearchDelegate<String> {
|
||||||
|
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"'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Consumer(
|
||||||
|
builder: (BuildContext context, WidgetRef ref, Widget? child) {
|
||||||
|
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: () {
|
||||||
|
ref.read(mediaProvider.notifier).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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,141 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
import 'package:f0ckapp/models/MediaItem.dart';
|
|
||||||
import 'package:f0ckapp/widgets/VideoOverlay.dart';
|
|
||||||
import 'package:f0ckapp/providers/MediaProvider.dart';
|
|
||||||
|
|
||||||
class VideoWidget extends ConsumerStatefulWidget {
|
|
||||||
final MediaItem details;
|
|
||||||
final bool isActive;
|
|
||||||
|
|
||||||
const VideoWidget({super.key, required this.details, required this.isActive});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<VideoWidget> createState() => _VideoWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoWidgetState extends ConsumerState<VideoWidget> {
|
|
||||||
late CachedVideoPlayerPlusController _controller;
|
|
||||||
bool _showControls = false;
|
|
||||||
Timer? _hideControlsTimer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_initController();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initController() async {
|
|
||||||
_controller = CachedVideoPlayerPlusController.networkUrl(
|
|
||||||
Uri.parse(widget.details.mediaUrl),
|
|
||||||
);
|
|
||||||
await _controller.initialize();
|
|
||||||
setState(() {});
|
|
||||||
|
|
||||||
_controller.addListener(() => setState(() {}));
|
|
||||||
|
|
||||||
if (widget.isActive) {
|
|
||||||
_controller.play();
|
|
||||||
}
|
|
||||||
_controller.setLooping(true);
|
|
||||||
|
|
||||||
final muted = ref.read(mediaProvider).muted;
|
|
||||||
_controller.setVolume(muted ? 0.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(covariant VideoWidget oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (widget.isActive != oldWidget.isActive) {
|
|
||||||
if (widget.isActive) {
|
|
||||||
_controller.play();
|
|
||||||
} else {
|
|
||||||
_controller.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
_hideControlsTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onTap({bool ctrlButton = false}) {
|
|
||||||
if (!ctrlButton) {
|
|
||||||
setState(() => _showControls = !_showControls);
|
|
||||||
}
|
|
||||||
if (_showControls) {
|
|
||||||
_hideControlsTimer?.cancel();
|
|
||||||
_hideControlsTimer = Timer(const Duration(seconds: 2), () {
|
|
||||||
setState(() => _showControls = false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final muted = ref.watch(mediaProvider).muted;
|
|
||||||
if (_controller.value.isInitialized &&
|
|
||||||
_controller.value.volume != (muted ? 0.0 : 1.0)) {
|
|
||||||
_controller.setVolume(muted ? 0.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isAudio = widget.details.mime.startsWith('audio');
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: _controller.value.isInitialized
|
|
||||||
? _controller.value.aspectRatio
|
|
||||||
: 9 / 16,
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
children: [
|
|
||||||
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) ...[
|
|
||||||
IgnorePointer(
|
|
||||||
ignoring: true,
|
|
||||||
child: Container(
|
|
||||||
color: Colors.black.withValues(alpha: 0.5),
|
|
||||||
width: double.infinity,
|
|
||||||
height: double.infinity,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VideoControlsOverlay(
|
|
||||||
controller: _controller,
|
|
||||||
button: () => _onTap(ctrlButton: true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
132
lib/widgets/end_drawer.dart
Normal file
132
lib/widgets/end_drawer.dart
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/providers/theme_provider.dart';
|
||||||
|
import 'package:f0ckapp/utils/appversion_util.dart';
|
||||||
|
|
||||||
|
class EndDrawer extends StatelessWidget {
|
||||||
|
final WidgetRef ref;
|
||||||
|
|
||||||
|
const EndDrawer({super.key, required this.ref});
|
||||||
|
|
||||||
|
void _showMsg(String message, BuildContext context) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
..removeCurrentSnackBar()
|
||||||
|
..showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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 String themeName = entry.key;
|
||||||
|
final ThemeData themeData = entry.value;
|
||||||
|
final ThemeData currentTheme = ref.watch(
|
||||||
|
themeNotifierProvider,
|
||||||
|
);
|
||||||
|
final bool 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: const Text('Einstellungen'),
|
||||||
|
onTap: () {
|
||||||
|
//context.go('/settings');
|
||||||
|
Navigator.pushReplacementNamed(context, '/settings');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text('v${AppVersion.version}'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_showMsg('jooong lass das, hier ist nichts', context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
62
lib/widgets/filter_bar.dart
Normal file
62
lib/widgets/filter_bar.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
|
||||||
|
class FilterBar extends StatelessWidget {
|
||||||
|
final MediaState mediaState;
|
||||||
|
final MediaNotifier mediaNotifier;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
const FilterBar({
|
||||||
|
super.key,
|
||||||
|
required this.mediaState,
|
||||||
|
required this.mediaNotifier,
|
||||||
|
required this.scrollController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
45
lib/widgets/media_tile.dart
Normal file
45
lib/widgets/media_tile.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/models/mediaitem_model.dart';
|
||||||
|
|
||||||
|
class MediaTile extends StatelessWidget {
|
||||||
|
final MediaItem item;
|
||||||
|
|
||||||
|
const MediaTile({super.key, required this.item});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pushNamed(context, '/${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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
192
lib/widgets/video_widget.dart
Normal file
192
lib/widgets/video_widget.dart
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:f0ckapp/models/mediaitem_model.dart';
|
||||||
|
import 'package:f0ckapp/widgets/videooverlay_widget.dart';
|
||||||
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
|
||||||
|
class VideoWidget extends ConsumerStatefulWidget {
|
||||||
|
final MediaItem details;
|
||||||
|
final bool isActive;
|
||||||
|
final bool fullScreen;
|
||||||
|
|
||||||
|
const VideoWidget({
|
||||||
|
super.key,
|
||||||
|
required this.details,
|
||||||
|
required this.isActive,
|
||||||
|
this.fullScreen = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<VideoWidget> createState() => _VideoWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoWidgetState extends ConsumerState<VideoWidget> {
|
||||||
|
late CachedVideoPlayerPlusController _controller;
|
||||||
|
bool _showControls = false;
|
||||||
|
Timer? _hideControlsTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initController();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initController() async {
|
||||||
|
_controller = CachedVideoPlayerPlusController.networkUrl(
|
||||||
|
Uri.parse(widget.details.mediaUrl),
|
||||||
|
);
|
||||||
|
await _controller.initialize();
|
||||||
|
setState(() {});
|
||||||
|
|
||||||
|
_controller.addListener(() => setState(() {}));
|
||||||
|
|
||||||
|
if (widget.isActive) {
|
||||||
|
_controller.play();
|
||||||
|
}
|
||||||
|
_controller.setLooping(true);
|
||||||
|
|
||||||
|
final bool muted = ref.read(mediaProvider).muted;
|
||||||
|
_controller.setVolume(muted ? 0.0 : 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant VideoWidget oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.isActive != oldWidget.isActive) {
|
||||||
|
if (widget.isActive) {
|
||||||
|
_controller.play();
|
||||||
|
} else {
|
||||||
|
_controller.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap({bool ctrlButton = false}) {
|
||||||
|
if (!ctrlButton) {
|
||||||
|
setState(() => _showControls = !_showControls);
|
||||||
|
}
|
||||||
|
if (_showControls) {
|
||||||
|
_hideControlsTimer?.cancel();
|
||||||
|
_hideControlsTimer = Timer(const Duration(seconds: 2), () {
|
||||||
|
setState(() => _showControls = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool muted = ref.watch(mediaProvider).muted;
|
||||||
|
if (_controller.value.isInitialized &&
|
||||||
|
_controller.value.volume != (muted ? 0.0 : 1.0)) {
|
||||||
|
_controller.setVolume(muted ? 0.0 : 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAudio = widget.details.mime.startsWith('audio');
|
||||||
|
|
||||||
|
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(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: _controller.value.isInitialized
|
||||||
|
? _controller.value.aspectRatio
|
||||||
|
: 9 / 16,
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
children: [
|
||||||
|
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) ...[
|
||||||
|
IgnorePointer(
|
||||||
|
ignoring: true,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VideoControlsOverlay(
|
||||||
|
controller: _controller,
|
||||||
|
button: () => _onTap(ctrlButton: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.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:f0ckapp/providers/MediaProvider.dart';
|
import 'package:f0ckapp/providers/media_provider.dart';
|
||||||
|
|
||||||
class VideoControlsOverlay extends ConsumerWidget {
|
class VideoControlsOverlay extends ConsumerWidget {
|
||||||
final CachedVideoPlayerPlusController controller;
|
final CachedVideoPlayerPlusController controller;
|
||||||
@ -17,8 +17,8 @@ class VideoControlsOverlay extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, ref) {
|
Widget build(BuildContext context, ref) {
|
||||||
final mediaState = ref.watch(mediaProvider);
|
final MediaState mediaState = ref.watch(mediaProvider);
|
||||||
final mediaNotifier = ref.read(mediaProvider.notifier);
|
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
40
pubspec.lock
40
pubspec.lock
@ -1,38 +1,6 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
app_links:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: app_links
|
|
||||||
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.4.0"
|
|
||||||
app_links_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_linux
|
|
||||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
app_links_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_platform_interface
|
|
||||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
app_links_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_web
|
|
||||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -272,14 +240,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
gtk:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: gtk
|
|
||||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -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.8+38
|
version: 1.1.23+53
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.9.0-100.2.beta
|
sdk: ^3.9.0-100.2.beta
|
||||||
@ -41,7 +41,6 @@ dependencies:
|
|||||||
share_plus: ^11.0.0
|
share_plus: ^11.0.0
|
||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
flutter_riverpod: ^2.6.1
|
flutter_riverpod: ^2.6.1
|
||||||
app_links: ^6.4.0
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -13,7 +13,7 @@ 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);
|
||||||
|
Reference in New Issue
Block a user