Compare commits

...

10 Commits

Author SHA1 Message Date
a4d50289c2 v1.1.19+49
All checks were successful
Flutter Schmutter / build (push) Successful in 3m36s
2025-06-11 14:53:26 +02:00
82fb23dbfd v1.1.18+48
All checks were successful
Flutter Schmutter / build (push) Successful in 3m36s
- fullscreen
2025-06-11 13:41:12 +02:00
13f957f016 test schmest
All checks were successful
Flutter Schmutter / build (push) Successful in 3m32s
2025-06-11 12:16:32 +02:00
707f14c5fb testbuild, rebranding
All checks were successful
Flutter Schmutter / build (push) Successful in 3m43s
2025-06-11 11:31:45 +02:00
493422e724 v1.1.15+45
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
- buildtest lol
2025-06-11 11:22:00 +02:00
3b95d128e1 fk gitea 2025-06-11 11:01:40 +02:00
57636c5de6 v1.1.14+44
All checks were successful
Flutter Schmutter / build (push) Successful in 3m32s
2025-06-11 10:52:15 +02:00
f75299f0d4 xd
All checks were successful
Flutter Schmutter / build (push) Successful in 3m53s
2025-06-10 18:53:52 +02:00
03c6431eca v1.1.13+43
- fk android
2025-06-10 18:53:07 +02:00
5876c809a5 v1.1.12+42
All checks were successful
Flutter Schmutter / build (push) Successful in 3m45s
- search schmearch
2025-06-10 11:07:00 +02:00
14 changed files with 397 additions and 222 deletions

View File

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

View File

@ -1,10 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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
android:label="f0ckapp"
android:label="f0ck"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
@ -16,8 +15,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:requestLegacyExternalStorage="true">
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues

View File

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

View File

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:f0ckapp/screens/mediagrid_screen.dart';
import 'package:f0ckapp/screens/detailview_screen.dart';
@ -14,7 +13,6 @@ import 'package:f0ckapp/providers/theme_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await FlutterDownloader.initialize();
await AppVersion.init();
runApp(ProviderScope(child: F0ckApp()));

View File

@ -3,12 +3,16 @@ class Suggestion {
final int tagged;
final double score;
Suggestion({required this.tag, required this.tagged, required this.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: int.tryParse(json['tagged'].toString()) ?? 0,
tagged: json['tagged'],
score: (json['score'] as num).toDouble(),
);
}

View File

@ -1,14 +1,14 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:f0ckapp/screens/fullscreen_screen.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.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:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
@ -72,50 +72,17 @@ class _DetailViewState extends ConsumerState<DetailView> {
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');
if (Platform.isAndroid || Platform.isIOS) {
PermissionStatus status = await Permission.storage.status;
if (!status.isGranted) {
status = await Permission.storage.request();
if (!status.isGranted) {
_showMsg("Speicherberechtigung wurde nicht erteilt.");
return;
}
}
}
bool? success = await methodChannel.invokeMethod<bool>('saveFile', {
'filePath': file.path,
'fileName': currentItem.dest,
});
String localPath;
if (Platform.isAndroid) {
final Directory? directory = await getExternalStorageDirectory();
localPath = "${directory!.path}/Download/fApp";
} else if (Platform.isIOS) {
final Directory directory = await getApplicationDocumentsDirectory();
localPath = directory.path;
} else {
final Directory directory = await getTemporaryDirectory();
localPath = directory.path;
}
final Directory savedDir = Directory(localPath);
if (!await savedDir.exists()) {
await savedDir.create(recursive: true);
}
try {
await FlutterDownloader.enqueue(
url: currentItem.mediaUrl,
savedDir: localPath,
fileName: currentItem.mediaUrl.split('/').last,
showNotification: true,
openFileFromNotification: true,
);
if (mounted) {
_showMsg('Download gestartet: ${currentItem.mediaUrl}');
}
} catch (e) {
_showMsg('Download fehlgeschlagen: $e');
}
success == true
? _showMsg('${currentItem.dest} wurde in Downloads/fApp neigespeichert.')
: _showMsg('${currentItem.dest} konnte nicht heruntergeladen werden.');
}
@override
@ -167,7 +134,13 @@ class _DetailViewState extends ConsumerState<DetailView> {
IconButton(
icon: const Icon(Icons.fullscreen),
onPressed: () {
_showMsg('fullscreen ist wip');
final mediaState = ref.read(mediaProvider);
final currentItem = mediaState.mediaItems[_currentIndex];
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => FullScreenMediaView(item: currentItem),
),
);
},
),
IconButton(

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:f0ckapp/models/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: 6.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),
onPressed: () => Navigator.of(context).pop(),
),
),
),
],
),
);
}
}

View File

@ -4,14 +4,12 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:f0ckapp/utils/appversion_util.dart';
import 'package:f0ckapp/providers/theme_provider.dart';
import 'package:f0ckapp/utils/customsearchdelegate_util.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
class MediaGrid extends ConsumerStatefulWidget {
const MediaGrid({super.key});
@ -56,8 +54,8 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
@override
Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier);
final MediaState mediaState = ref.watch(mediaProvider);
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return Scaffold(
key: _scaffoldKey,
@ -80,14 +78,12 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
showSearch(
icon: const Icon(Icons.search),
onPressed: () async {
await showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
//mediaNotifier.setTag('drachenlord');
//_scrollController.jumpTo(0);
},
),
IconButton(
@ -163,7 +159,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
),
child: null,
),
ExpansionTile(
/*ExpansionTile(
title: const Text('Login'),
children: [
Padding(
@ -193,7 +189,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
const SnackBar(
content: Text("noch nicht implementiert lol"),
),
/*final success = await login(
final success = await login(
_usernameController.text,
_passwordController.text,
);
@ -204,7 +200,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login fehlgeschlagen!")),
);
}*/
}
);
},
child: const Text('Login'),
@ -213,7 +209,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
),
),
],
),
),*/
ExpansionTile(
title: const Text('Theme'),
children: [
@ -221,10 +217,10 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
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;
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,
@ -284,11 +280,11 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
),
itemCount:
mediaState.mediaItems.length + (mediaState.isLoading ? 1 : 0),
itemBuilder: (context, index) {
itemBuilder: (BuildContext context, int index) {
if (index >= mediaState.mediaItems.length) {
return const Center(child: CircularProgressIndicator());
}
final item = mediaState.mediaItems[index];
final MediaItem item = mediaState.mediaItems[index];
return InkWell(
onTap: () async {

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
@ -6,7 +7,9 @@ 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 = FlutterSecureStorage();
final FlutterSecureStorage storage = const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
);
Future<List<MediaItem>> fetchMedia({
int? older,
@ -25,7 +28,7 @@ Future<List<MediaItem>> fetchMedia({
},
);
final response = await http.get(url);
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();
@ -37,7 +40,7 @@ Future<List<MediaItem>> fetchMedia({
Future<MediaItem> fetchMediaDetail(int itemId) async {
final Uri url = Uri.parse('https://api.f0ck.me/item/${itemId.toString()}');
final response = await http.get(url);
final http.Response response = await http.get(url);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(response.body);
@ -50,39 +53,45 @@ Future<MediaItem> fetchMediaDetail(int itemId) async {
}
Future<List<Suggestion>> fetchSuggestions(String query) async {
final Uri uri = Uri.parse(
'https://f0ck.me/api/v2/admin/tags/suggest?q=$query',
); // wip: new route in pyapi
final response = await http.get(uri);
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 Map<String, dynamic> decoded = jsonDecode(response.body);
if (decoded['success'] == true && decoded.containsKey('suggestions')) {
final List<dynamic> suggestionsList = decoded['suggestions'];
return suggestionsList
.map(
(dynamic jsonItem) =>
Suggestion.fromJson(jsonItem as Map<String, dynamic>),
)
.toList()
..sort(
(Suggestion a, Suggestion b) =>
(b.score * b.tagged).compareTo(a.score * a.tagged),
);
if (response.statusCode == 200) {
final dynamic decoded = jsonDecode(response.body);
if (decoded is List) {
return decoded
.map((item) => Suggestion.fromJson(item as Map<String, dynamic>))
.toList()
..sort((a, b) => b.score.compareTo(a.score));
} else {
throw Exception('Unerwartetes Format: Erwartet wurde eine Liste.');
}
} 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('Nichts gefunden.');
throw Exception(
'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
);
}
} 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 beim Verarbeiten der Anfrage: $e');
}
}
Future<bool> login(String username, String password) async {
final Uri url = Uri.parse('https://api.f0ck.me/login');
final response = await http.post(
final http.Response response = await http.post(
url,
body: {'username': username, 'password': password},
);

View File

@ -1,20 +1,25 @@
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: Icon(Icons.clear),
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
_clearResults();
showSuggestions(context);
},
),
@ -24,8 +29,11 @@ class CustomSearchDelegate extends SearchDelegate<String> {
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => close(context, 'null'),
icon: const Icon(Icons.arrow_back),
onPressed: () {
_debounceTimer?.cancel();
close(context, 'null');
},
);
}
@ -36,35 +44,58 @@ class CustomSearchDelegate extends SearchDelegate<String> {
@override
Widget buildSuggestions(BuildContext context) {
if (query.isEmpty) {
return Container(padding: EdgeInsets.all(16.0), child: Text(''));
}
final Future<List<Suggestion>> futureSuggestions = Future.delayed(
Duration(milliseconds: 300),
() => fetchSuggestions(query),
);
return FutureBuilder<List<Suggestion>>(
future: futureSuggestions,
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text("Fehler: ${snapshot.error}"));
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text("Keine Vorschläge gefunden."));
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."));
}
final List<Suggestion> suggestions = snapshot.data!;
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
return ListView.builder(
itemCount: suggestions.length,
itemCount: _suggestions!.length,
itemBuilder: (BuildContext context, int index) {
final Suggestion suggestion = suggestions[index];
final Suggestion suggestion = _suggestions![index];
return ListTile(
title: Text(suggestion.tag),
subtitle: Text(
@ -83,4 +114,32 @@ class CustomSearchDelegate extends SearchDelegate<String> {
},
);
}
Widget _buildLoadingIndicator() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(strokeWidth: 3.0),
const SizedBox(height: 12),
const Text(
'Vorschläge werden geladen...',
style: TextStyle(fontStyle: FontStyle.italic),
),
],
);
}
void _clearResults() {
_debounceTimer?.cancel();
_suggestions = null;
_isLoading = false;
_error = null;
_lastFetchedQuery = "";
}
@override
void close(BuildContext context, String result) {
_debounceTimer?.cancel();
super.close(context, result);
}
}

View File

@ -13,8 +13,14 @@ 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});
const VideoWidget({
super.key,
required this.details,
required this.isActive,
this.fullScreen = false,
});
@override
ConsumerState<VideoWidget> createState() => _VideoWidgetState();
@ -90,17 +96,15 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
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(
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(
@ -118,24 +122,71 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
? 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,
),
),
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),
),
),
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),
),
],
],
],
),
),
),
],
);
],
);
}
}
}

View File

@ -150,14 +150,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_downloader:
dependency: "direct main"
description:
name: flutter_downloader
sha256: "93a9ddbd561f8a3f5483b4189453fba145a0a1014a88143c96a966296b78a118"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
flutter_lints:
dependency: "direct dev"
description:
@ -393,7 +385,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@ -440,54 +432,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
url: "https://pub.dev"
source: hosted
version: "12.0.0+1"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
url: "https://pub.dev"
source: hosted
version: "13.0.1"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
url: "https://pub.dev"
source: hosted
version: "9.4.7"
permission_handler_html:
dependency: transitive
description:
name: permission_handler_html
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
url: "https://pub.dev"
source: hosted
version: "0.1.3+5"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
url: "https://pub.dev"
source: hosted
version: "4.3.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
platform:
dependency: transitive
description:

View File

@ -1,5 +1,5 @@
name: f0ckapp
description: "A new Flutter project."
description: "f0ck schm0ck"
# The following line prevents the package from being accidentally published to
# 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
@ -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
# 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.
version: 1.1.11+41
version: 1.1.19+49
environment:
sdk: ^3.9.0-100.2.beta
@ -42,9 +42,6 @@ dependencies:
flutter_secure_storage: ^9.2.4
flutter_riverpod: ^2.6.1
go_router: ^15.1.3
flutter_downloader: ^1.12.0
permission_handler: ^12.0.0+1
path_provider: ^2.1.5
dev_dependencies:
flutter_test:

View File

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