Compare commits
5 Commits
v1.1.8+38
...
v1.1.11+41
Author | SHA1 | Date | |
---|---|---|---|
c35308fbc1 | |||
e945844151 | |||
74eb6e3d26 | |||
9755066d1e | |||
671b3cfbe0 |
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)
|
||||
- [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).
|
||||
|
@ -1,6 +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"
|
||||
|
@ -1,36 +1,89 @@
|
||||
import 'package:app_links/app_links.dart';
|
||||
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();
|
||||
final Uri? initialUri = await AppLinks().getInitialLink();
|
||||
|
||||
runApp(ProviderScope(child: F0ckApp(initialUri: initialUri)));
|
||||
runApp(ProviderScope(child: F0ckApp()));
|
||||
}
|
||||
|
||||
class F0ckApp extends ConsumerWidget {
|
||||
final Uri? initialUri;
|
||||
const F0ckApp({super.key, this.initialUri});
|
||||
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: MediaGrid(initialUri: initialUri),
|
||||
);
|
||||
},
|
||||
final ThemeData theme = ref.watch(themeNotifierProvider);
|
||||
return MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: _router,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
15
lib/models/suggestion_model.dart
Normal file
15
lib/models/suggestion_model.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,16 +102,21 @@ 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({int? id}) async {
|
||||
//if (state.isLoading) return;
|
||||
if (id != null) {
|
||||
print('requested id: ${id.toString()}');
|
||||
}
|
||||
|
||||
if (state.isLoading) return;
|
||||
state = state.replace(isLoading: true);
|
||||
try {
|
||||
final older =
|
@ -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';
|
||||
|
@ -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,53 +56,123 @@ 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.fullscreen),
|
||||
icon: const Icon(Icons.fullscreen),
|
||||
onPressed: () {
|
||||
// wip
|
||||
_showMsg('fullscreen ist wip');
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.download),
|
||||
onPressed: () {
|
||||
// wip
|
||||
},
|
||||
icon: const Icon(Icons.download),
|
||||
onPressed: _downloadMedia,
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
@ -170,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(
|
||||
@ -189,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);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -200,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(
|
||||
@ -224,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),
|
@ -1,21 +1,19 @@
|
||||
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: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/utils/ParseDeepLink.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"];
|
||||
|
||||
class MediaGrid extends ConsumerStatefulWidget {
|
||||
final Uri? initialUri = null;
|
||||
const MediaGrid({super.key, required initialUri});
|
||||
const MediaGrid({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<MediaGrid> createState() => _MediaGridState();
|
||||
@ -28,8 +26,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
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()
|
||||
@ -48,23 +44,6 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
|
||||
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
|
||||
@ -100,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,
|
||||
@ -302,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,
|
@ -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);
|
||||
|
@ -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,*/
|
86
lib/utils/customsearchdelegate_util.dart
Normal file
86
lib/utils/customsearchdelegate_util.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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,8 +17,8 @@ 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,
|
108
pubspec.lock
108
pubspec.lock
@ -1,38 +1,6 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -182,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:
|
||||
@ -272,14 +248,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: gtk
|
||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
||||
name: go_router
|
||||
sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "15.1.3"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -344,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:
|
||||
@ -409,7 +393,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||
@ -456,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:
|
||||
|
@ -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.8+38
|
||||
version: 1.1.11+41
|
||||
|
||||
environment:
|
||||
sdk: ^3.9.0-100.2.beta
|
||||
@ -41,7 +41,10 @@ dependencies:
|
||||
share_plus: ^11.0.0
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_riverpod: ^2.6.1
|
||||
app_links: ^6.4.0
|
||||
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:
|
||||
|
Reference in New Issue
Block a user