diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fe518f2..be66168 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ - + diff --git a/lib/main.dart b/lib/main.dart index a0146ac..a58e6fc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,36 +1,87 @@ -import 'package:app_links/app_links.dart'; +import 'package:f0ckapp/providers/MediaProvider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:f0ckapp/screens/MediaGrid.dart'; +import 'package:f0ckapp/screens/DetailView.dart'; import 'package:f0ckapp/utils/AppVersion.dart'; import 'package:f0ckapp/providers/ThemeProvider.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - void main() async { WidgetsFlutterBinding.ensureInitialized(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 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: (context, state) { + return const MediaGrid(); + }, + ), + GoRoute( + path: '/:rest(.*)', + builder: (context, state) { + final isInternalLink = (state.extra is bool && state.extra == true); + final fullPath = state.matchedLocation; + + final regExp = RegExp( + r'^(?:/tag/(?.+?))?(?:/(?image|audio|video))?(?:/(?\d+))?$', + ); + final 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 preloadOffset = 50; + + return Consumer( + builder: (context, ref, child) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final 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 theme = ref.watch(themeNotifierProvider); + return MaterialApp.router( + debugShowCheckedModeBanner: false, + routerConfig: _router, + theme: theme, ); } } diff --git a/lib/providers/MediaProvider.dart b/lib/providers/MediaProvider.dart index da77a88..aae4f22 100644 --- a/lib/providers/MediaProvider.dart +++ b/lib/providers/MediaProvider.dart @@ -107,11 +107,7 @@ class MediaNotifier extends StateNotifier { } Future 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 = diff --git a/lib/screens/DetailView.dart b/lib/screens/DetailView.dart index 399ea23..af8d4cd 100644 --- a/lib/screens/DetailView.dart +++ b/lib/screens/DetailView.dart @@ -2,10 +2,10 @@ 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:share_plus/share_plus.dart'; import 'package:f0ckapp/models/MediaItem.dart'; @@ -24,26 +24,13 @@ class DetailView extends ConsumerStatefulWidget { } class _DetailViewState extends ConsumerState { - 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 { @@ -81,37 +68,60 @@ class _DetailViewState extends ConsumerState { @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 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 + _showError('fullscreen ist wip'); }, ), IconButton( - icon: Icon(Icons.download), + icon: const Icon(Icons.download), onPressed: () { - // wip + _showError('download ist wip'); }, ), PopupMenuButton( @@ -170,7 +180,7 @@ class _DetailViewState extends ConsumerState { body: Stack( children: [ PageTransformer( - controller: _pageController, + controller: _pageController!, pages: mediaState.mediaItems.map((item) { int itemIndex = mediaState.mediaItems.indexOf(item); return SafeArea( @@ -189,8 +199,8 @@ class _DetailViewState extends ConsumerState { child: InputChip( label: Text(mediaState.tag!), onDeleted: () { - mediaNotifier.setTag(null); - Navigator.pop(context); + ref.read(mediaProvider.notifier).setTag(null); + context.go('/', extra: true); }, ), ), @@ -224,7 +234,7 @@ class _DetailViewState extends ConsumerState { 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), diff --git a/lib/screens/MediaGrid.dart b/lib/screens/MediaGrid.dart index 05a0791..6c07148 100644 --- a/lib/screens/MediaGrid.dart +++ b/lib/screens/MediaGrid.dart @@ -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: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:go_router/go_router.dart'; const List mediaTypes = ["alles", "image", "video", "audio"]; const List 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 createState() => _MediaGridState(); @@ -28,8 +26,6 @@ class _MediaGridState extends ConsumerState { 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 { 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; - 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; - await handleComplexDeepLink(initparams, context, ref, _scrollController); - }); } @override @@ -302,15 +281,7 @@ class _MediaGridState extends ConsumerState { 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, diff --git a/pubspec.lock b/pubspec.lock index eda825c..d86de63 100644 --- a/pubspec.lock +++ b/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: @@ -272,14 +240,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 +312,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: diff --git a/pubspec.yaml b/pubspec.yaml index 81e72fa..64d61c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.9+39 environment: sdk: ^3.9.0-100.2.beta @@ -41,7 +41,7 @@ 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 dev_dependencies: flutter_test: