Compare commits

..

4 Commits

Author SHA1 Message Date
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
46 changed files with 239 additions and 59 deletions

View File

@ -1,5 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application <application
android:label="f0ckapp" android:label="f0ckapp"
@ -28,12 +29,26 @@
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="true"/>
<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> </activity>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and 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,87 @@
import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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/MediaGrid.dart';
import 'package:f0ckapp/screens/DetailView.dart';
import 'package:f0ckapp/utils/AppVersion.dart'; import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart'; import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await AppVersion.init(); await AppVersion.init();
runApp(ProviderScope(child: const F0ckApp())); runApp(ProviderScope(child: F0ckApp()));
} }
class F0ckApp extends ConsumerWidget { class F0ckApp extends ConsumerWidget {
const F0ckApp({super.key}); 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/(?<tag>.+?))?(?:/(?<mime>image|audio|video))?(?:/(?<itemid>\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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Consumer( final theme = ref.watch(themeNotifierProvider);
builder: (context, ref, _) { return MaterialApp.router(
return MaterialApp( debugShowCheckedModeBanner: false,
debugShowCheckedModeBanner: false, routerConfig: _router,
theme: ref.watch(themeNotifierProvider), theme: theme,
home: const MediaGrid(),
);
},
); );
} }
} }

View File

@ -106,14 +106,12 @@ class MediaNotifier extends StateNotifier<MediaState> {
state = state.replace(mediaItems: updated); state = state.replace(mediaItems: updated);
} }
Future<void> loadMedia() async { Future<void> loadMedia({int? id}) async {
if (state.isLoading) return; if (state.isLoading) return;
state = state.replace(isLoading: true); state = state.replace(isLoading: true);
try { try {
final older = state.mediaItems.isNotEmpty final older =
? state.mediaItems.last.id id ?? (state.mediaItems.isNotEmpty ? state.mediaItems.last.id : null);
: null;
final newMedia = await fetchMedia( final newMedia = await fetchMedia(
older: older, older: older,
type: mediaTypes[state.typeIndex], type: mediaTypes[state.typeIndex],

View File

@ -2,10 +2,10 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.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:share_plus/share_plus.dart';
import 'package:f0ckapp/models/MediaItem.dart'; import 'package:f0ckapp/models/MediaItem.dart';
@ -24,26 +24,13 @@ class DetailView extends ConsumerStatefulWidget {
} }
class _DetailViewState extends ConsumerState<DetailView> { class _DetailViewState extends ConsumerState<DetailView> {
late PageController _pageController; PageController? _pageController;
bool isLoading = false; bool isLoading = false;
int _currentIndex = 0; int _currentIndex = 0;
@override @override
void initState() { void initState() {
super.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 { void _preloadAdjacentMedia(int index) async {
@ -81,31 +68,60 @@ class _DetailViewState extends ConsumerState<DetailView> {
@override @override
void dispose() { void dispose() {
_pageController.dispose(); _pageController?.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaState = ref.watch(mediaProvider); 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( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: const Center(child: CircularProgressIndicator()), 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text('f0ck #${mediaState.mediaItems[_currentIndex].id}'), 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: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.download), icon: const Icon(Icons.fullscreen),
onPressed: () { onPressed: () {
// wip _showError('fullscreen ist wip');
},
),
IconButton(
icon: const Icon(Icons.download),
onPressed: () {
_showError('download ist wip');
}, },
), ),
PopupMenuButton<String>( PopupMenuButton<String>(
@ -164,7 +180,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
body: Stack( body: Stack(
children: [ children: [
PageTransformer( PageTransformer(
controller: _pageController, controller: _pageController!,
pages: mediaState.mediaItems.map((item) { pages: mediaState.mediaItems.map((item) {
int itemIndex = mediaState.mediaItems.indexOf(item); int itemIndex = mediaState.mediaItems.indexOf(item);
return SafeArea( return SafeArea(
@ -183,8 +199,8 @@ class _DetailViewState extends ConsumerState<DetailView> {
child: InputChip( child: InputChip(
label: Text(mediaState.tag!), label: Text(mediaState.tag!),
onDeleted: () { onDeleted: () {
mediaNotifier.setTag(null); ref.read(mediaProvider.notifier).setTag(null);
Navigator.pop(context); context.go('/', extra: true);
}, },
), ),
), ),
@ -218,7 +234,7 @@ class _DetailViewState extends ConsumerState<DetailView> {
if (tag.tag == 'sfw' || tag.tag == 'nsfw') return; if (tag.tag == 'sfw' || tag.tag == 'nsfw') return;
setState(() { setState(() {
mediaNotifier.setTag(tag.tag); mediaNotifier.setTag(tag.tag);
Navigator.pop(context, true); context.go('/', extra: true);
}); });
}, },
label: Text(tag.tag), label: Text(tag.tag),

View File

@ -7,6 +7,7 @@ import 'package:f0ckapp/screens/DetailView.dart';
import 'package:f0ckapp/providers/MediaProvider.dart'; import 'package:f0ckapp/providers/MediaProvider.dart';
import 'package:f0ckapp/utils/AppVersion.dart'; import 'package:f0ckapp/utils/AppVersion.dart';
import 'package:f0ckapp/providers/ThemeProvider.dart'; import 'package:f0ckapp/providers/ThemeProvider.dart';
import 'package:go_router/go_router.dart';
const List<String> mediaTypes = ["alles", "image", "video", "audio"]; const List<String> mediaTypes = ["alles", "image", "video", "audio"];
const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"]; const List<String> mediaModes = ["sfw", "nsfw", "untagged", "all"];
@ -280,15 +281,7 @@ class _MediaGridState extends ConsumerState<MediaGrid> {
return InkWell( return InkWell(
onTap: () async { onTap: () async {
bool? ret = await Navigator.push( context.push('/${item.id}', extra: true);
context,
MaterialPageRoute(
builder: (context) => DetailView(initialItemId: item.id),
),
);
if (ret != null && ret) {
_scrollController.jumpTo(0);
}
}, },
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,

View File

@ -0,0 +1,94 @@
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,*/

View File

@ -25,7 +25,7 @@ class VideoControlsOverlay extends ConsumerWidget {
children: [ children: [
Positioned( Positioned(
right: 12, right: 12,
top: 12, bottom: 12,
child: _ControlButton( child: _ControlButton(
mediaState.muted ? Icons.volume_off : Icons.volume_up, mediaState.muted ? Icons.volume_off : Icons.volume_up,
() { () {
@ -35,13 +35,6 @@ class VideoControlsOverlay extends ConsumerWidget {
size: 16, size: 16,
), ),
), ),
Positioned(
right: 12,
bottom: 12,
child: _ControlButton(Icons.fullscreen, () {
button();
}, size: 16),
),
Center( Center(
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -240,6 +240,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" 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: html:
dependency: transitive dependency: transitive
description: description:
@ -304,6 +312,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.0" version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: 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 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.1.6+36 version: 1.1.9+39
environment: environment:
sdk: ^3.9.0-100.2.beta sdk: ^3.9.0-100.2.beta
@ -41,6 +41,7 @@ dependencies:
share_plus: ^11.0.0 share_plus: ^11.0.0
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
go_router: ^15.1.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: