Compare commits

...

8 Commits

Author SHA1 Message Date
c35308fbc1 v1.1.11+41
All checks were successful
Flutter Schmutter / build (push) Successful in 3m57s
- fixed: duplicates on the frontpage
- new: search by tag
2025-06-10 08:39:55 +02:00
e945844151 v1.1.10+40
All checks were successful
Flutter Schmutter / build (push) Successful in 3m56s
- download button lel
2025-06-09 19:08:23 +02:00
74eb6e3d26 readme & license 2025-06-09 15:42:13 +02:00
9755066d1e full retard renaming 2025-06-09 15:04:03 +02:00
671b3cfbe0 v1.1.9+39
All checks were successful
Flutter Schmutter / build (push) Successful in 3m35s
2025-06-09 14:02:59 +02:00
93fb3536ee v1.1.8+38
All checks were successful
Flutter Schmutter / build (push) Successful in 3m48s
- blah
2025-06-08 19:40:06 +02:00
346e447d5e v1.1.7+37
All checks were successful
Flutter Schmutter / build (push) Successful in 3m44s
- worst update eu west
2025-06-08 17:16:10 +02:00
f7777821fd iiiiiicon 2025-06-08 10:33:05 +02:00
56 changed files with 505 additions and 101 deletions

21
LICENSE Normal file
View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,33 +1,89 @@
import 'package:f0ckapp/providers/media_provider.dart';
import 'package:flutter/material.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: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';
import 'package:f0ckapp/utils/appversion_util.dart';
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: const F0ckApp()));
runApp(ProviderScope(child: F0ckApp()));
}
class F0ckApp extends ConsumerWidget {
const F0ckApp({super.key});
F0ckApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
return const MediaGrid();
},
),
GoRoute(
path: '/:rest(.*)',
builder: (context, state) {
final bool isInternalLink = (state.extra is bool && state.extra == true);
final String fullPath = state.matchedLocation;
final regExp = RegExp(
r'^(?:/tag/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\d+))?$',
);
final RegExpMatch? match = regExp.firstMatch(fullPath);
if (match == null) {
return const Scaffold(body: Center(child: Text('Ungültiger Link')));
}
final String? tag = match.namedGroup('tag');
final String? mime = match.namedGroup('mime');
final String? idStr = match.namedGroup('itemid');
final int? itemId = idStr != null ? int.tryParse(idStr) : null;
const int preloadOffset = 50;
return Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
if (!isInternalLink) {
mediaNotifier.setType(mime ?? "alles");
mediaNotifier.setTag(tag);
}
if (itemId != null) {
await mediaNotifier.loadMedia(id: itemId + preloadOffset);
}
});
if (itemId != null) {
return DetailView(initialItemId: itemId);
} else {
return MediaGrid();
}
},
);
},
),
],
);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Consumer(
builder: (context, ref, _) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ref.watch(themeNotifierProvider),
home: const MediaGrid(),
);
},
final ThemeData theme = ref.watch(themeNotifierProvider);
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: _router,
theme: theme,
);
}
}

View File

@ -0,0 +1,15 @@
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: int.tryParse(json['tagged'].toString()) ?? 0,
score: (json['score'] as num).toDouble(),
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/services/Api.dart';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/services/api_service.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
@ -102,18 +102,25 @@ class MediaNotifier extends StateNotifier<MediaState> {
}
void addMediaItems(List<MediaItem> newItems) {
final updated = List<MediaItem>.from(state.mediaItems)..addAll(newItems);
state = state.replace(mediaItems: updated);
final Set<int> existingIds = state.mediaItems
.map((item) => item.id)
.toSet();
final List<MediaItem> filteredItems = newItems
.where((item) => !existingIds.contains(item.id))
.toList();
if (filteredItems.isNotEmpty) {
final List<MediaItem> updated = List<MediaItem>.from(state.mediaItems)
..addAll(filteredItems);
state = state.replace(mediaItems: updated);
}
}
Future<void> loadMedia() async {
Future<void> loadMedia({int? id}) async {
if (state.isLoading) return;
state = state.replace(isLoading: true);
try {
final older = state.mediaItems.isNotEmpty
? state.mediaItems.last.id
: null;
final older =
id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
final newMedia = await fetchMedia(
older: older,
type: mediaTypes[state.typeIndex],

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

View File

@ -2,17 +2,20 @@ import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:f0ckapp/models/MediaItem.dart';
import 'package:f0ckapp/widgets/VideoWidget.dart';
import 'package:f0ckapp/utils/SmartRefreshIndicator.dart';
import 'package:f0ckapp/utils/PageTransformer.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package: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;
@ -24,26 +27,13 @@ class DetailView extends ConsumerStatefulWidget {
}
class _DetailViewState extends ConsumerState<DetailView> {
late PageController _pageController;
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 {
@ -66,48 +56,124 @@ class _DetailViewState extends ConsumerState<DetailView> {
try {
await ref.read(mediaProvider.notifier).loadMedia();
} catch (e) {
_showError("Fehler beim Laden der Medien: $e");
_showMsg("Fehler beim Laden der Medien: $e");
} finally {
setState(() => isLoading = false);
}
}
void _showError(String message) {
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];
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;
}
}
}
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');
}
}
@override
void dispose() {
_pageController.dispose();
_pageController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider);
final mediaNotifier = ref.read(mediaProvider.notifier);
final MediaState mediaState = ref.watch(mediaProvider);
final int itemIndex = mediaState.mediaItems.indexWhere(
(item) => item.id == widget.initialItemId,
);
if (mediaState.mediaItems.isEmpty) {
if (itemIndex == -1) {
Future.microtask(() {
ref
.read(mediaProvider.notifier)
.loadMedia(id: widget.initialItemId + 50);
});
return Scaffold(
appBar: AppBar(),
body: const Center(child: CircularProgressIndicator()),
);
}
if (_pageController == null) {
_pageController = PageController(initialPage: itemIndex);
_currentIndex = itemIndex;
_pageController!.addListener(() {
setState(() => _currentIndex = _pageController!.page?.round() ?? 0);
});
_preloadAdjacentMedia(itemIndex);
}
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go('/', extra: true);
},
),
actions: [
IconButton(
icon: Icon(Icons.download),
icon: const Icon(Icons.fullscreen),
onPressed: () {
// wip
_showMsg('fullscreen ist wip');
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: _downloadMedia,
),
PopupMenuButton<String>(
onSelected: (value) async {
final item = mediaState.mediaItems[_currentIndex];
@ -164,7 +230,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
body: Stack(
children: [
PageTransformer(
controller: _pageController,
controller: _pageController!,
pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea(
@ -183,8 +249,8 @@ class _DetailViewState extends ConsumerState<DetailView> {
child: InputChip(
label: Text(mediaState.tag!),
onDeleted: () {
mediaNotifier.setTag(null);
Navigator.pop(context);
ref.read(mediaProvider.notifier).setTag(null);
context.go('/', extra: true);
},
),
),
@ -194,7 +260,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
}
Widget _buildMediaItem(MediaItem item, bool isActive) {
final mediaNotifier = ref.read(mediaProvider.notifier);
final MediaNotifier mediaNotifier = ref.read(mediaProvider.notifier);
return SingleChildScrollView(
child: Column(
@ -218,7 +284,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
setState(() {
mediaNotifier.setTag(tag.tag);
Navigator.pop(context, true);
context.go('/', extra: true);
});
},
label: Text(tag.tag),

View File

@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:f0ckapp/screens/DetailView.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.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"];
@ -78,6 +79,17 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
},
),
actions: [
IconButton(
icon: Icon(Icons.search),
onPressed: () {
showSearch(
context: context,
delegate: CustomSearchDelegate(),
);
//mediaNotifier.setTag('drachenlord');
//_scrollController.jumpTo(0);
},
),
IconButton(
icon: Icon(
mediaState.random ? Icons.shuffle_on_outlined : Icons.shuffle,
@ -280,15 +292,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
return InkWell(
onTap: () async {
bool? ret = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailView(initialItemId: item.id),
),
);
if (ret != null && ret) {
_scrollController.jumpTo(0);
}
context.push('/${item.id}', extra: true);
},
child: Stack(
fit: StackFit.expand,

View File

@ -3,9 +3,10 @@ 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';
import 'package:f0ckapp/models/mediaitem_model.dart';
import 'package:f0ckapp/models/suggestion_model.dart';
final storage = FlutterSecureStorage();
final FlutterSecureStorage storage = FlutterSecureStorage();
Future<List<MediaItem>> fetchMedia({
int? older,
@ -48,6 +49,36 @@ 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);
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),
);
} else {
throw Exception('Nichts gefunden.');
}
} else {
throw Exception(
'Fehler beim Abrufen der Vorschläge: ${response.statusCode}',
);
}
}
Future<bool> login(String username, String password) async {
final Uri url = Uri.parse('https://api.f0ck.me/login');
@ -57,8 +88,8 @@ Future<bool> login(String username, String password) async {
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final token = data['token'];
final dynamic data = jsonDecode(response.body);
final dynamic token = data['token'];
await storage.write(key: "token", value: token);

View File

@ -0,0 +1,86 @@
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> {
@override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
icon: Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
),
];
}
@override
Widget buildLeading(BuildContext context) {
return IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => close(context, 'null'),
);
}
@override
Widget buildResults(BuildContext context) {
return Center(child: Text('Suchergebnisse für: "$query"'));
}
@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."));
}
final List<Suggestion> suggestions = snapshot.data!;
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);
},
);
},
);
},
);
},
);
}
}

View File

@ -6,9 +6,9 @@ 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';
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;
@ -45,7 +45,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
}
_controller.setLooping(true);
final muted = ref.read(mediaProvider).muted;
final bool muted = ref.read(mediaProvider).muted;
_controller.setVolume(muted ? 0.0 : 1.0);
}
@ -82,7 +82,7 @@ class _VideoWidgetState extends ConsumerState<VideoWidget> {
@override
Widget build(BuildContext context) {
final muted = ref.watch(mediaProvider).muted;
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);

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_video_player_plus/cached_video_player_plus.dart';
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/providers/media_provider.dart';
class VideoControlsOverlay extends ConsumerWidget {
final CachedVideoPlayerPlusController controller;
@ -17,15 +17,15 @@ class VideoControlsOverlay extends ConsumerWidget {
@override
Widget build(BuildContext context, ref) {
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 Stack(
alignment: Alignment.center,
children: [
Positioned(
right: 12,
top: 12,
bottom: 12,
child: _ControlButton(
mediaState.muted ? Icons.volume_off : Icons.volume_up,
() {
@ -35,13 +35,6 @@ class VideoControlsOverlay extends ConsumerWidget {
size: 16,
),
),
Positioned(
right: 12,
bottom: 12,
child: _ControlButton(Icons.fullscreen, () {
button();
}, size: 16),
),
Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,

View File

@ -150,6 +150,14 @@ 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:
@ -240,6 +248,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175
url: "https://pub.dev"
source: hosted
version: "15.1.3"
html:
dependency: transitive
description:
@ -304,6 +320,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
@ -369,7 +393,7 @@ packages:
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@ -416,6 +440,54 @@ 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

@ -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.6+36
version: 1.1.11+41
environment:
sdk: ^3.9.0-100.2.beta
@ -41,6 +41,10 @@ dependencies:
share_plus: ^11.0.0
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: