Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28a63483da |
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.4-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.4" /></a>
|
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.5-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.5" /></a>
|
||||||
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
||||||
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
|
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
|
||||||
<a href="https://github.com/RarDog/Prisma/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
|
<a href="https://github.com/RarDog/Prisma/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.4-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.4" /></a>
|
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.5-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.5" /></a>
|
||||||
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
||||||
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
|
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
|
||||||
<a href="https://github.com/RarDog/Prisma/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
|
<a href="https://github.com/RarDog/Prisma/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../backend/models/post.dart';
|
||||||
|
import '../features/post/presentation/post_details_screen.dart';
|
||||||
|
import '../features/post/presentation/similar_posts_screen.dart';
|
||||||
|
|
||||||
|
class AppNavigator {
|
||||||
|
const AppNavigator._();
|
||||||
|
|
||||||
|
static void openPost(
|
||||||
|
BuildContext context, {
|
||||||
|
required Post post,
|
||||||
|
List<Post>? postsList,
|
||||||
|
}) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (ctx) => PostDetailsScreen(
|
||||||
|
providerId: post.providerId,
|
||||||
|
postId: post.id,
|
||||||
|
initialPost: post,
|
||||||
|
postsList: postsList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void openSimilarPosts(
|
||||||
|
BuildContext context, {
|
||||||
|
required Post post,
|
||||||
|
}) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (ctx) => SimilarPostsScreen(
|
||||||
|
providerId: post.providerId,
|
||||||
|
postId: post.id,
|
||||||
|
initialPost: post,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
const appDisplayVersion = '3.5.4';
|
const appDisplayVersion = '3.5.5';
|
||||||
const appBuildNumber = 80;
|
const appBuildNumber = 81;
|
||||||
const appReleaseChannel = 'Stable';
|
const appReleaseChannel = 'Stable';
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
const prismaChangelog = [
|
const prismaChangelog = [
|
||||||
|
PrismaChange(
|
||||||
|
version: '3.5.5',
|
||||||
|
title: 'Сохранение последней вкладки и состояния навигации между разделами',
|
||||||
|
bullets: [
|
||||||
|
'Сохранение последней вкладки: приложение запоминает активный раздел при выходе или закрытии и автоматически открывает его при следующем запуске.',
|
||||||
|
'Независимая история и сохранение места: при переключении между вкладками состояние каждого раздела полностью сохраняется. Например, если открыть публикацию или карточку у артиста и переключиться в поиск или ленту, при возврате обратно вы останетесь ровно на том же посте.',
|
||||||
|
'Бесшовные переходы между ветками навигации: улучшена обработка системной кнопки «Назад», переход по разделам стал еще быстрее и плавнее.',
|
||||||
|
],
|
||||||
|
),
|
||||||
PrismaChange(
|
PrismaChange(
|
||||||
version: '3.5.4',
|
version: '3.5.4',
|
||||||
title: 'Исправление распознавания MP4-видео и отключение устаревших сервисов Kemono/Coomer',
|
title: 'Исправление распознавания MP4-видео и отключение устаревших сервисов Kemono/Coomer',
|
||||||
|
|||||||
+153
-98
@@ -21,116 +21,171 @@ import '../features/settings/presentation/settings_screen.dart';
|
|||||||
import '../features/viewed/presentation/viewed_screen.dart';
|
import '../features/viewed/presentation/viewed_screen.dart';
|
||||||
import '../shared/widgets/app_shell.dart';
|
import '../shared/widgets/app_shell.dart';
|
||||||
|
|
||||||
|
final branchNavKeys = List.generate(8, (_) => GlobalKey<NavigatorState>());
|
||||||
|
|
||||||
final appRouterProvider = Provider<GoRouter>((ref) {
|
final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: '/',
|
initialLocation: '/',
|
||||||
routes: [
|
routes: [
|
||||||
ShellRoute(
|
StatefulShellRoute.indexedStack(
|
||||||
builder: (context, state, child) => AppShell(child: child),
|
builder: (context, state, navigationShell) => AppShell(
|
||||||
|
navigationShell: navigationShell,
|
||||||
|
child: navigationShell,
|
||||||
|
),
|
||||||
|
branches: [
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[0],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
pageBuilder: (context, state) => _transitionPage(
|
||||||
|
state,
|
||||||
|
child: FeedScreen(initialQuery: state.uri.queryParameters['q']),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[1],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/search',
|
||||||
|
builder: (context, state) => const SearchScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[2],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/favorites',
|
||||||
|
builder: (context, state) => const FavoritesScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[3],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/viewed',
|
||||||
|
builder: (context, state) => const ViewedScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[4],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/collections',
|
||||||
|
builder: (context, state) => const CollectionsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: ':collectionId',
|
||||||
|
builder: (context, state) => CollectionDetailsScreen(
|
||||||
|
collectionId: state.pathParameters['collectionId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[5],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/artists',
|
||||||
|
builder: (context, state) => const ArtistsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: ':providerId/:service/:artistId',
|
||||||
|
builder: (context, state) => ArtistPostsScreen(
|
||||||
|
providerId: state.pathParameters['providerId']!,
|
||||||
|
service: state.pathParameters['service']!,
|
||||||
|
artistId: state.pathParameters['artistId']!,
|
||||||
|
artistName: state.uri.queryParameters['name'] ??
|
||||||
|
state.pathParameters['artistId']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[6],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/providers',
|
||||||
|
builder: (context, state) => const ProvidersScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'new',
|
||||||
|
builder: (context, state) => ProviderFormScreen(
|
||||||
|
initialConfig: state.extra is ContentProviderConfig
|
||||||
|
? state.extra! as ContentProviderConfig
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'check',
|
||||||
|
builder: (context, state) => const ProviderCheckScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StatefulShellBranch(
|
||||||
|
navigatorKey: branchNavKeys[7],
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/settings',
|
||||||
|
builder: (context, state) => const SettingsScreen(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'hidden',
|
||||||
|
builder: (context, state) => const HiddenPostsScreen(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'cache',
|
||||||
|
builder: (context, state) => const CacheManagerScreen(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/post/:providerId/:postId',
|
||||||
|
builder: (context, state) {
|
||||||
|
final extra = state.extra;
|
||||||
|
final Post? initialPost;
|
||||||
|
final List<Post>? postsList;
|
||||||
|
if (extra is PostNavigationContext) {
|
||||||
|
initialPost = extra.currentPost;
|
||||||
|
postsList = extra.posts;
|
||||||
|
} else if (extra is Post) {
|
||||||
|
initialPost = extra;
|
||||||
|
postsList = null;
|
||||||
|
} else {
|
||||||
|
initialPost = null;
|
||||||
|
postsList = null;
|
||||||
|
}
|
||||||
|
return PostDetailsScreen(
|
||||||
|
providerId: state.pathParameters['providerId']!,
|
||||||
|
postId: state.pathParameters['postId']!,
|
||||||
|
initialPost: initialPost,
|
||||||
|
postsList: postsList,
|
||||||
|
);
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: 'similar',
|
||||||
pageBuilder: (context, state) => _transitionPage(
|
|
||||||
state,
|
|
||||||
child: FeedScreen(initialQuery: state.uri.queryParameters['q']),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/search',
|
|
||||||
builder: (context, state) => const SearchScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/post/:providerId/:postId',
|
|
||||||
builder: (context, state) {
|
|
||||||
final extra = state.extra;
|
|
||||||
final Post? initialPost;
|
|
||||||
final List<Post>? postsList;
|
|
||||||
if (extra is PostNavigationContext) {
|
|
||||||
initialPost = extra.currentPost;
|
|
||||||
postsList = extra.posts;
|
|
||||||
} else if (extra is Post) {
|
|
||||||
initialPost = extra;
|
|
||||||
postsList = null;
|
|
||||||
} else {
|
|
||||||
initialPost = null;
|
|
||||||
postsList = null;
|
|
||||||
}
|
|
||||||
return PostDetailsScreen(
|
|
||||||
providerId: state.pathParameters['providerId']!,
|
|
||||||
postId: state.pathParameters['postId']!,
|
|
||||||
initialPost: initialPost,
|
|
||||||
postsList: postsList,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/post/:providerId/:postId/similar',
|
|
||||||
builder: (context, state) => SimilarPostsScreen(
|
builder: (context, state) => SimilarPostsScreen(
|
||||||
providerId: state.pathParameters['providerId']!,
|
providerId: state.pathParameters['providerId']!,
|
||||||
postId: state.pathParameters['postId']!,
|
postId: state.pathParameters['postId']!,
|
||||||
initialPost: state.extra is Post ? state.extra! as Post : null,
|
initialPost: state.extra is Post ? state.extra! as Post : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/favorites',
|
|
||||||
builder: (context, state) => const FavoritesScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/viewed',
|
|
||||||
builder: (context, state) => const ViewedScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/collections',
|
|
||||||
builder: (context, state) => const CollectionsScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/collections/:collectionId',
|
|
||||||
builder: (context, state) => CollectionDetailsScreen(
|
|
||||||
collectionId: state.pathParameters['collectionId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/artists',
|
|
||||||
builder: (context, state) => const ArtistsScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/artists/:providerId/:service/:artistId',
|
|
||||||
builder: (context, state) => ArtistPostsScreen(
|
|
||||||
providerId: state.pathParameters['providerId']!,
|
|
||||||
service: state.pathParameters['service']!,
|
|
||||||
artistId: state.pathParameters['artistId']!,
|
|
||||||
artistName: state.uri.queryParameters['name'] ??
|
|
||||||
state.pathParameters['artistId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/providers',
|
|
||||||
builder: (context, state) => const ProvidersScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/providers/new',
|
|
||||||
builder: (context, state) => ProviderFormScreen(
|
|
||||||
initialConfig: state.extra is ContentProviderConfig
|
|
||||||
? state.extra! as ContentProviderConfig
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/providers/check',
|
|
||||||
builder: (context, state) => const ProviderCheckScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/settings',
|
|
||||||
builder: (context, state) => const SettingsScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/settings/hidden',
|
|
||||||
builder: (context, state) => const HiddenPostsScreen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/settings/cache',
|
|
||||||
builder: (context, state) => const CacheManagerScreen(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -64,8 +64,10 @@ class AppSettings {
|
|||||||
this.downloadPathTemplate = '{Artist}/{ID}',
|
this.downloadPathTemplate = '{Artist}/{ID}',
|
||||||
this.searchHistoryLimit = 500,
|
this.searchHistoryLimit = 500,
|
||||||
this.tagCacheLimit = 5000,
|
this.tagCacheLimit = 5000,
|
||||||
|
this.lastActiveLocation = '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String lastActiveLocation;
|
||||||
final List<String> favoriteArtists;
|
final List<String> favoriteArtists;
|
||||||
final List<String> pawchiveAccounts;
|
final List<String> pawchiveAccounts;
|
||||||
final bool pawchiveBidirectionalSync;
|
final bool pawchiveBidirectionalSync;
|
||||||
@@ -165,6 +167,7 @@ class AppSettings {
|
|||||||
lastFeedRating: null,
|
lastFeedRating: null,
|
||||||
searchHistoryLimit: 500,
|
searchHistoryLimit: 500,
|
||||||
tagCacheLimit: 5000,
|
tagCacheLimit: 5000,
|
||||||
|
lastActiveLocation: '/',
|
||||||
);
|
);
|
||||||
|
|
||||||
AppSettings copyWith({
|
AppSettings copyWith({
|
||||||
@@ -220,8 +223,10 @@ class AppSettings {
|
|||||||
String? downloadPathTemplate,
|
String? downloadPathTemplate,
|
||||||
int? searchHistoryLimit,
|
int? searchHistoryLimit,
|
||||||
int? tagCacheLimit,
|
int? tagCacheLimit,
|
||||||
|
String? lastActiveLocation,
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
|
lastActiveLocation: lastActiveLocation ?? this.lastActiveLocation,
|
||||||
favoriteArtists: favoriteArtists ?? this.favoriteArtists,
|
favoriteArtists: favoriteArtists ?? this.favoriteArtists,
|
||||||
pawchiveAccounts: pawchiveAccounts ?? this.pawchiveAccounts,
|
pawchiveAccounts: pawchiveAccounts ?? this.pawchiveAccounts,
|
||||||
pawchiveBidirectionalSync:
|
pawchiveBidirectionalSync:
|
||||||
@@ -336,6 +341,7 @@ class AppSettings {
|
|||||||
'downloadPathTemplate': downloadPathTemplate,
|
'downloadPathTemplate': downloadPathTemplate,
|
||||||
'searchHistoryLimit': searchHistoryLimit,
|
'searchHistoryLimit': searchHistoryLimit,
|
||||||
'tagCacheLimit': tagCacheLimit,
|
'tagCacheLimit': tagCacheLimit,
|
||||||
|
'lastActiveLocation': lastActiveLocation,
|
||||||
};
|
};
|
||||||
|
|
||||||
factory AppSettings.fromJson(Map<String, dynamic> json) => AppSettings(
|
factory AppSettings.fromJson(Map<String, dynamic> json) => AppSettings(
|
||||||
@@ -455,6 +461,8 @@ class AppSettings {
|
|||||||
defaults.searchHistoryLimit,
|
defaults.searchHistoryLimit,
|
||||||
tagCacheLimit: (json['tagCacheLimit'] as num?)?.toInt() ??
|
tagCacheLimit: (json['tagCacheLimit'] as num?)?.toInt() ??
|
||||||
defaults.tagCacheLimit,
|
defaults.tagCacheLimit,
|
||||||
|
lastActiveLocation: (json['lastActiveLocation'] as String?) ??
|
||||||
|
defaults.lastActiveLocation,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<PawchiveAccount> get parsedPawchiveAccounts {
|
List<PawchiveAccount> get parsedPawchiveAccounts {
|
||||||
@@ -545,6 +553,14 @@ class SettingsService {
|
|||||||
return updateSettings(settings.copyWith(nsfwEnabled: enabled));
|
return updateSettings(settings.copyWith(nsfwEnabled: enabled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result<void>> saveLastActiveLocation(String location) async {
|
||||||
|
final result = await getSettings();
|
||||||
|
if (result is Error<AppSettings>) return Error(result.failure);
|
||||||
|
final settings = (result as Success<AppSettings>).data;
|
||||||
|
if (settings.lastActiveLocation == location) return const Success(null);
|
||||||
|
return updateSettings(settings.copyWith(lastActiveLocation: location));
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result<void>> saveCacheSettings({
|
Future<Result<void>> saveCacheSettings({
|
||||||
required int ttlHours,
|
required int ttlHours,
|
||||||
required int maxItems,
|
required int maxItems,
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
@@ -791,12 +791,10 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
|
|||||||
settings.mediaQualityMode),
|
settings.mediaQualityMode),
|
||||||
favoriteKeys: favoriteKeys,
|
favoriteKeys: favoriteKeys,
|
||||||
viewedKeys: viewedKeys,
|
viewedKeys: viewedKeys,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: displayedPosts,
|
||||||
posts: displayedPosts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onFavorite: (post) async {
|
onFavorite: (post) async {
|
||||||
if (favoriteKeys.contains(post.cacheKey)) {
|
if (favoriteKeys.contains(post.cacheKey)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
@@ -1574,12 +1575,10 @@ class _FavoriteArtistMediaStrip extends ConsumerWidget {
|
|||||||
final isVideo = MediaUrlSelector.isVideo(post);
|
final isVideo = MediaUrlSelector.isVideo(post);
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
onTap: () => context.push(
|
onTap: () => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: posts,
|
||||||
posts: posts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
@@ -167,12 +168,10 @@ class CollectionDetailsScreen extends ConsumerWidget {
|
|||||||
mediaQualityMode:
|
mediaQualityMode:
|
||||||
MediaQualityMode.fromName(settings.mediaQualityMode),
|
MediaQualityMode.fromName(settings.mediaQualityMode),
|
||||||
favoriteKeys: favoriteKeys,
|
favoriteKeys: favoriteKeys,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: items,
|
||||||
posts: items,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onFavorite: (post) async {
|
onFavorite: (post) async {
|
||||||
if (favoriteKeys.contains(post.cacheKey)) {
|
if (favoriteKeys.contains(post.cacheKey)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
@@ -970,12 +971,10 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
|
|||||||
mediaQualityMode: MediaQualityMode.fromName(settings.mediaQualityMode),
|
mediaQualityMode: MediaQualityMode.fromName(settings.mediaQualityMode),
|
||||||
favoriteKeys: posts.map((post) => post.cacheKey).toSet(),
|
favoriteKeys: posts.map((post) => post.cacheKey).toSet(),
|
||||||
downloadedKeys: downloadedKeys,
|
downloadedKeys: downloadedKeys,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: posts,
|
||||||
posts: posts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onFavorite: (post) =>
|
onFavorite: (post) =>
|
||||||
ref.read(favoritesControllerProvider.notifier).remove(post),
|
ref.read(favoritesControllerProvider.notifier).remove(post),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'dart:async';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
@@ -331,12 +332,10 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
|||||||
selectionMode: _selectionMode,
|
selectionMode: _selectionMode,
|
||||||
selectedKeys: _selectedKeys,
|
selectedKeys: _selectedKeys,
|
||||||
gridMode: settings.gridMode,
|
gridMode: settings.gridMode,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: state.posts,
|
||||||
posts: state.posts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onPreview: (post) => _showPreview(context, post),
|
onPreview: (post) => _showPreview(context, post),
|
||||||
onToggleSelected: (post) => _toggleSelected(post),
|
onToggleSelected: (post) => _toggleSelected(post),
|
||||||
@@ -392,7 +391,7 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
|
|||||||
void _openRandom(List<Post> posts) {
|
void _openRandom(List<Post> posts) {
|
||||||
if (posts.isEmpty) return;
|
if (posts.isEmpty) return;
|
||||||
final post = posts[Random().nextInt(posts.length)];
|
final post = posts[Random().nextInt(posts.length)];
|
||||||
context.push('/post/${post.providerId}/${post.id}', extra: post);
|
AppNavigator.openPost(context, post: post, postsList: posts);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applySearchQuery(String query) async {
|
Future<void> _applySearchQuery(String query) async {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/app_strings.dart';
|
import '../../../app/app_strings.dart';
|
||||||
import '../../../app/motion.dart';
|
import '../../../app/motion.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
@@ -872,7 +873,7 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _openSimilar(BuildContext context, WidgetRef ref, Post post) {
|
void _openSimilar(BuildContext context, WidgetRef ref, Post post) {
|
||||||
context.push('/post/${post.providerId}/${post.id}/similar', extra: post);
|
AppNavigator.openSimilarPosts(context, post: post);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openSourcePage(WidgetRef ref, Post post) async {
|
Future<void> _openSourcePage(WidgetRef ref, Post post) async {
|
||||||
@@ -888,7 +889,16 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _replacePost(BuildContext context, Post post) {
|
void _replacePost(BuildContext context, Post post) {
|
||||||
context.replace('/post/${post.providerId}/${post.id}', extra: post);
|
Navigator.of(context).pushReplacement(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (ctx) => PostDetailsScreen(
|
||||||
|
providerId: post.providerId,
|
||||||
|
postId: post.id,
|
||||||
|
initialPost: post,
|
||||||
|
postsList: postsList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _close(BuildContext context) {
|
void _close(BuildContext context) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
@@ -98,9 +98,9 @@ class SimilarPostsScreen extends ConsumerWidget {
|
|||||||
MediaQualityMode.fromName(settings.mediaQualityMode),
|
MediaQualityMode.fromName(settings.mediaQualityMode),
|
||||||
favoriteKeys: favorites,
|
favoriteKeys: favorites,
|
||||||
viewedKeys: viewed,
|
viewedKeys: viewed,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: post,
|
post: post,
|
||||||
),
|
),
|
||||||
onFavorite: (post) =>
|
onFavorite: (post) =>
|
||||||
_toggleFavorite(ref, post, favorites),
|
_toggleFavorite(ref, post, favorites),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../core/utils/result.dart';
|
import '../../../core/utils/result.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
@@ -62,9 +62,9 @@ class HiddenPostsScreen extends ConsumerWidget {
|
|||||||
item: item,
|
item: item,
|
||||||
onOpen: item.post == null
|
onOpen: item.post == null
|
||||||
? null
|
? null
|
||||||
: () => context.push(
|
: () => AppNavigator.openPost(
|
||||||
'/post/${item.post!.providerId}/${item.post!.id}',
|
context,
|
||||||
extra: item.post,
|
post: item.post!,
|
||||||
),
|
),
|
||||||
onRestore: () async {
|
onRestore: () async {
|
||||||
await ref
|
await ref
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../../app/app.dart';
|
import '../../../app/app.dart';
|
||||||
|
import '../../../app/app_navigator.dart';
|
||||||
import '../../../app/responsive.dart';
|
import '../../../app/responsive.dart';
|
||||||
import '../../../backend/backend.dart';
|
import '../../../backend/backend.dart';
|
||||||
import '../../../shared/widgets/adaptive_scaffold.dart';
|
import '../../../shared/widgets/adaptive_scaffold.dart';
|
||||||
@@ -142,12 +143,10 @@ class _ViewedScreenState extends ConsumerState<ViewedScreen> {
|
|||||||
mediaQualityMode:
|
mediaQualityMode:
|
||||||
MediaQualityMode.fromName(settings.mediaQualityMode),
|
MediaQualityMode.fromName(settings.mediaQualityMode),
|
||||||
favoriteKeys: favoriteKeys,
|
favoriteKeys: favoriteKeys,
|
||||||
onOpen: (post) => context.push(
|
onOpen: (post) => AppNavigator.openPost(
|
||||||
'/post/${post.providerId}/${post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: post,
|
||||||
currentPost: post,
|
postsList: allPosts,
|
||||||
posts: allPosts,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onFavorite: (post) => _toggleFavorite(
|
onFavorite: (post) => _toggleFavorite(
|
||||||
ref,
|
ref,
|
||||||
@@ -260,12 +259,10 @@ class _ViewedScreenState extends ConsumerState<ViewedScreen> {
|
|||||||
isFavorite: favoriteKeys.contains(item.post.cacheKey),
|
isFavorite: favoriteKeys.contains(item.post.cacheKey),
|
||||||
isRu: isRu,
|
isRu: isRu,
|
||||||
onOpen: () {
|
onOpen: () {
|
||||||
context.push(
|
AppNavigator.openPost(
|
||||||
'/post/${item.post.providerId}/${item.post.id}',
|
context,
|
||||||
extra: PostNavigationContext(
|
post: item.post,
|
||||||
currentPost: item.post,
|
postsList: allPosts,
|
||||||
posts: allPosts,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onFavorite: () => _toggleFavorite(
|
onFavorite: () => _toggleFavorite(
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../../app/app.dart';
|
import '../../app/app.dart';
|
||||||
import '../../app/motion.dart';
|
import '../../app/motion.dart';
|
||||||
import '../../app/responsive.dart';
|
import '../../app/responsive.dart';
|
||||||
|
import '../../app/router.dart';
|
||||||
import '../../backend/backend.dart';
|
import '../../backend/backend.dart';
|
||||||
import '../../core/utils/result.dart';
|
import '../../core/utils/result.dart';
|
||||||
|
|
||||||
class AppShell extends ConsumerStatefulWidget {
|
class AppShell extends ConsumerStatefulWidget {
|
||||||
const AppShell({required this.child, super.key});
|
const AppShell({
|
||||||
|
required this.child,
|
||||||
|
this.navigationShell,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final StatefulNavigationShell? navigationShell;
|
||||||
|
|
||||||
static const destinations = [
|
static const destinations = [
|
||||||
_Destination('feed', 'Feed', Icons.dashboard_rounded, '/'),
|
_Destination('feed', 'Feed', Icons.dashboard_rounded, '/'),
|
||||||
@@ -33,6 +39,30 @@ class AppShell extends ConsumerStatefulWidget {
|
|||||||
_Destination('settings', 'Settings', Icons.settings_rounded, '/settings'),
|
_Destination('settings', 'Settings', Icons.settings_rounded, '/settings'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static int branchIndexForLocation(String location) {
|
||||||
|
if (location.startsWith('/settings')) return 7;
|
||||||
|
if (location.startsWith('/providers')) return 6;
|
||||||
|
if (location.startsWith('/artists')) return 5;
|
||||||
|
if (location.startsWith('/collections')) return 4;
|
||||||
|
if (location.startsWith('/viewed')) return 3;
|
||||||
|
if (location.startsWith('/favorites')) return 2;
|
||||||
|
if (location.startsWith('/search')) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String locationForBranchIndex(int index) {
|
||||||
|
return switch (index) {
|
||||||
|
1 => '/search',
|
||||||
|
2 => '/favorites',
|
||||||
|
3 => '/viewed',
|
||||||
|
4 => '/collections',
|
||||||
|
5 => '/artists',
|
||||||
|
6 => '/providers',
|
||||||
|
7 => '/settings',
|
||||||
|
_ => '/',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static List<_Destination> _visibleDestinations(
|
static List<_Destination> _visibleDestinations(
|
||||||
AppSettings settings,
|
AppSettings settings,
|
||||||
bool hasArtists,
|
bool hasArtists,
|
||||||
@@ -52,11 +82,70 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
final List<String> _tabHistory = [];
|
final List<String> _tabHistory = [];
|
||||||
String? _currentPath;
|
String? _currentPath;
|
||||||
bool _isBackNavigating = false;
|
bool _isBackNavigating = false;
|
||||||
|
bool _restoredTab = false;
|
||||||
|
late final AppLifecycleListener _lifecycleListener;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_lifecycleListener = AppLifecycleListener(
|
||||||
|
onStateChange: (state) {
|
||||||
|
if (state == AppLifecycleState.paused ||
|
||||||
|
state == AppLifecycleState.inactive ||
|
||||||
|
state == AppLifecycleState.detached) {
|
||||||
|
_saveCurrentLocation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final settings = ref.read(appSettingsProvider).value;
|
||||||
|
_maybeRestoreLastActiveTab(settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_lifecycleListener.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _saveCurrentLocation([String? location]) {
|
||||||
|
final loc = location ??
|
||||||
|
_currentPath ??
|
||||||
|
(widget.navigationShell != null
|
||||||
|
? AppShell.locationForBranchIndex(
|
||||||
|
widget.navigationShell!.currentIndex)
|
||||||
|
: '/');
|
||||||
|
ref.read(settingsServiceProvider).saveLastActiveLocation(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _maybeRestoreLastActiveTab(AppSettings? settings) {
|
||||||
|
if (_restoredTab || settings == null) return;
|
||||||
|
final lastLocation = settings.lastActiveLocation;
|
||||||
|
if (lastLocation.isNotEmpty && lastLocation != '/') {
|
||||||
|
final idx = AppShell.branchIndexForLocation(lastLocation);
|
||||||
|
if (idx > 0 && widget.navigationShell != null) {
|
||||||
|
_restoredTab = true;
|
||||||
|
widget.navigationShell!.goBranch(idx, initialLocation: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
final path = GoRouterState.of(context).uri.path;
|
final path = GoRouterState.of(context).uri.path;
|
||||||
|
_syncPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant AppShell oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final path = GoRouterState.of(context).uri.path;
|
||||||
|
_syncPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncPath(String path) {
|
||||||
if (_isBackNavigating) {
|
if (_isBackNavigating) {
|
||||||
_isBackNavigating = false;
|
_isBackNavigating = false;
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
@@ -74,10 +163,37 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_currentPath = path;
|
_currentPath = path;
|
||||||
|
_saveCurrentLocation(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onNavigate(String location) {
|
||||||
|
final targetIndex = AppShell.branchIndexForLocation(location);
|
||||||
|
if (widget.navigationShell != null) {
|
||||||
|
if (widget.navigationShell!.currentIndex == targetIndex) {
|
||||||
|
branchNavKeys[targetIndex]
|
||||||
|
.currentState
|
||||||
|
?.popUntil((route) => route.isFirst);
|
||||||
|
} else {
|
||||||
|
widget.navigationShell!.goBranch(
|
||||||
|
targetIndex,
|
||||||
|
initialLocation: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context.go(location);
|
||||||
|
}
|
||||||
|
_saveCurrentLocation(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _handleBack() {
|
bool _handleBack() {
|
||||||
if (Navigator.of(context).canPop()) {
|
final shell = widget.navigationShell;
|
||||||
|
if (shell != null) {
|
||||||
|
final currentBranchKey = branchNavKeys[shell.currentIndex];
|
||||||
|
if (currentBranchKey.currentState?.canPop() ?? false) {
|
||||||
|
currentBranchKey.currentState?.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (Navigator.of(context).canPop()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -90,13 +206,13 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
if (_tabHistory.isNotEmpty) {
|
if (_tabHistory.isNotEmpty) {
|
||||||
final previous = _tabHistory.removeLast();
|
final previous = _tabHistory.removeLast();
|
||||||
_isBackNavigating = true;
|
_isBackNavigating = true;
|
||||||
context.go(previous);
|
_onNavigate(previous);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPath != '/') {
|
if (currentPath != '/' && (shell == null || shell.currentIndex != 0)) {
|
||||||
_isBackNavigating = true;
|
_isBackNavigating = true;
|
||||||
context.go('/');
|
_onNavigate('/');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +221,12 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
ref.listen<AsyncValue<AppSettings>>(appSettingsProvider, (prev, next) {
|
||||||
|
next.whenData((settings) {
|
||||||
|
_maybeRestoreLastActiveTab(settings);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
final settings =
|
final settings =
|
||||||
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
||||||
final artistConfigs =
|
final artistConfigs =
|
||||||
@@ -147,7 +269,7 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
actions: {
|
actions: {
|
||||||
_NavigateIntent: CallbackAction<_NavigateIntent>(
|
_NavigateIntent: CallbackAction<_NavigateIntent>(
|
||||||
onInvoke: (intent) {
|
onInvoke: (intent) {
|
||||||
context.go(intent.location);
|
_onNavigate(intent.location);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -164,11 +286,15 @@ class _AppShellState extends ConsumerState<AppShell> {
|
|||||||
? _DesktopShell(
|
? _DesktopShell(
|
||||||
destinations: destinations,
|
destinations: destinations,
|
||||||
ru: ru,
|
ru: ru,
|
||||||
|
currentBranchIndex: widget.navigationShell?.currentIndex,
|
||||||
|
onNavigate: _onNavigate,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
)
|
)
|
||||||
: _MobileShell(
|
: _MobileShell(
|
||||||
destinations: destinations,
|
destinations: destinations,
|
||||||
ru: ru,
|
ru: ru,
|
||||||
|
currentBranchIndex: widget.navigationShell?.currentIndex,
|
||||||
|
onNavigate: _onNavigate,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -183,10 +309,14 @@ class _DesktopShell extends StatefulWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.destinations,
|
required this.destinations,
|
||||||
required this.ru,
|
required this.ru,
|
||||||
|
required this.onNavigate,
|
||||||
|
this.currentBranchIndex,
|
||||||
});
|
});
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<_Destination> destinations;
|
final List<_Destination> destinations;
|
||||||
final bool ru;
|
final bool ru;
|
||||||
|
final ValueChanged<String> onNavigate;
|
||||||
|
final int? currentBranchIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_DesktopShell> createState() => _DesktopShellState();
|
State<_DesktopShell> createState() => _DesktopShellState();
|
||||||
@@ -198,10 +328,10 @@ class _DesktopShellState extends State<_DesktopShell> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final location = GoRouterState.of(context).uri.path;
|
final location = GoRouterState.of(context).uri.path;
|
||||||
|
final activeBranch = widget.currentBranchIndex ??
|
||||||
|
AppShell.branchIndexForLocation(location);
|
||||||
final selected = widget.destinations.indexWhere(
|
final selected = widget.destinations.indexWhere(
|
||||||
(item) => item.location == '/'
|
(item) => AppShell.branchIndexForLocation(item.location) == activeBranch,
|
||||||
? location == '/'
|
|
||||||
: location.startsWith(item.location),
|
|
||||||
);
|
);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Row(
|
body: Row(
|
||||||
@@ -229,7 +359,7 @@ class _DesktopShellState extends State<_DesktopShell> {
|
|||||||
selected: (selected < 0 ? 0 : selected) == index,
|
selected: (selected < 0 ? 0 : selected) == index,
|
||||||
expanded: _hovered,
|
expanded: _hovered,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.go(widget.destinations[index].location);
|
widget.onNavigate(widget.destinations[index].location);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -334,10 +464,14 @@ class _MobileShell extends StatelessWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.destinations,
|
required this.destinations,
|
||||||
required this.ru,
|
required this.ru,
|
||||||
|
required this.onNavigate,
|
||||||
|
this.currentBranchIndex,
|
||||||
});
|
});
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final List<_Destination> destinations;
|
final List<_Destination> destinations;
|
||||||
final bool ru;
|
final bool ru;
|
||||||
|
final ValueChanged<String> onNavigate;
|
||||||
|
final int? currentBranchIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -347,10 +481,10 @@ class _MobileShell extends StatelessWidget {
|
|||||||
.take(6)
|
.take(6)
|
||||||
.toList();
|
.toList();
|
||||||
final location = GoRouterState.of(context).uri.path;
|
final location = GoRouterState.of(context).uri.path;
|
||||||
|
final activeBranch = currentBranchIndex ??
|
||||||
|
AppShell.branchIndexForLocation(location);
|
||||||
final selected = items.indexWhere(
|
final selected = items.indexWhere(
|
||||||
(item) => item.location == '/'
|
(item) => AppShell.branchIndexForLocation(item.location) == activeBranch,
|
||||||
? location == '/'
|
|
||||||
: location.startsWith(item.location),
|
|
||||||
);
|
);
|
||||||
final isKeyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
|
final isKeyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
|
||||||
final bottomInset = isKeyboardOpen
|
final bottomInset = isKeyboardOpen
|
||||||
@@ -368,13 +502,13 @@ class _MobileShell extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
floatingActionButton: (location == '/settings' || isKeyboardOpen)
|
floatingActionButton: (activeBranch == 7 || isKeyboardOpen)
|
||||||
? null
|
? null
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 0, right: 2),
|
padding: const EdgeInsets.only(bottom: 0, right: 2),
|
||||||
child: _LiquidGlassSettingsButton(
|
child: _LiquidGlassSettingsButton(
|
||||||
ru: ru,
|
ru: ru,
|
||||||
onTap: () => context.go('/settings'),
|
onTap: () => onNavigate('/settings'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
|
||||||
@@ -383,7 +517,7 @@ class _MobileShell extends StatelessWidget {
|
|||||||
: _LiquidGlassBottomBar(
|
: _LiquidGlassBottomBar(
|
||||||
selectedIndex: selected < 0 ? 0 : selected,
|
selectedIndex: selected < 0 ? 0 : selected,
|
||||||
onDestinationSelected: (index) =>
|
onDestinationSelected: (index) =>
|
||||||
context.go(items[index].location),
|
onNavigate(items[index].location),
|
||||||
items: items,
|
items: items,
|
||||||
ru: ru,
|
ru: ru,
|
||||||
),
|
),
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 3.5.4+80
|
version: 3.5.5+81
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gel_rule_app/backend/backend.dart';
|
||||||
|
import 'package:gel_rule_app/shared/widgets/app_shell.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
group('Tab Navigation & State Preservation Tests', () {
|
||||||
|
test('AppShell branchIndexForLocation and locationForBranchIndex map correctly', () {
|
||||||
|
expect(AppShell.branchIndexForLocation('/'), 0);
|
||||||
|
expect(AppShell.branchIndexForLocation('/?q=test'), 0);
|
||||||
|
expect(AppShell.branchIndexForLocation('/search'), 1);
|
||||||
|
expect(AppShell.branchIndexForLocation('/favorites'), 2);
|
||||||
|
expect(AppShell.branchIndexForLocation('/viewed'), 3);
|
||||||
|
expect(AppShell.branchIndexForLocation('/collections'), 4);
|
||||||
|
expect(AppShell.branchIndexForLocation('/collections/123'), 4);
|
||||||
|
expect(AppShell.branchIndexForLocation('/artists'), 5);
|
||||||
|
expect(AppShell.branchIndexForLocation('/artists/pawchive/patreon/99'), 5);
|
||||||
|
expect(AppShell.branchIndexForLocation('/providers'), 6);
|
||||||
|
expect(AppShell.branchIndexForLocation('/settings'), 7);
|
||||||
|
expect(AppShell.branchIndexForLocation('/settings/hidden'), 7);
|
||||||
|
|
||||||
|
expect(AppShell.locationForBranchIndex(0), '/');
|
||||||
|
expect(AppShell.locationForBranchIndex(1), '/search');
|
||||||
|
expect(AppShell.locationForBranchIndex(2), '/favorites');
|
||||||
|
expect(AppShell.locationForBranchIndex(3), '/viewed');
|
||||||
|
expect(AppShell.locationForBranchIndex(4), '/collections');
|
||||||
|
expect(AppShell.locationForBranchIndex(5), '/artists');
|
||||||
|
expect(AppShell.locationForBranchIndex(6), '/providers');
|
||||||
|
expect(AppShell.locationForBranchIndex(7), '/settings');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('AppSettings preserves lastActiveLocation in JSON serialization', () {
|
||||||
|
const defaults = AppSettings.defaults;
|
||||||
|
expect(defaults.lastActiveLocation, '/');
|
||||||
|
|
||||||
|
final updated = defaults.copyWith(lastActiveLocation: '/artists/pawchive/patreon/12345');
|
||||||
|
expect(updated.lastActiveLocation, '/artists/pawchive/patreon/12345');
|
||||||
|
|
||||||
|
final json = updated.toJson();
|
||||||
|
expect(json['lastActiveLocation'], '/artists/pawchive/patreon/12345');
|
||||||
|
|
||||||
|
final restored = AppSettings.fromJson(json);
|
||||||
|
expect(restored.lastActiveLocation, '/artists/pawchive/patreon/12345');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user