Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ae802f89c |
@@ -10,7 +10,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Version-2.0.2-blueviolet?style=for-the-badge" alt="Version 2.0.2" />
|
<img src="https://img.shields.io/badge/Version-2.0.3-blueviolet?style=for-the-badge" alt="Version 2.0.3" />
|
||||||
<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-brightgreen?style=for-the-badge" alt="Platforms" />
|
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" />
|
||||||
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
|
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
- **⚡ Сверхбыстрые видео-превью**: Мгновенная загрузка превью без фризов и лишней нагрузки на процессор и память смартфона.
|
- **⚡ Сверхбыстрые видео-превью**: Мгновенная загрузка превью без фризов и лишней нагрузки на процессор и память смартфона.
|
||||||
- **↔️ Листание постов влево и вправо**: Полноэкранный пейджер с плавными свайпами между фото и видео во всех разделах (Лента, Авторы, Избранное, Коллекции, История просмотров).
|
- **↔️ Листание постов влево и вправо**: Полноэкранный пейджер с плавными свайпами между фото и видео во всех разделах (Лента, Авторы, Избранное, Коллекции, История просмотров).
|
||||||
- **🖼️ Мульти-медиа в одном посте**: Удобная карусель с индикатором `1 / 4` для постов с несколькими файлами.
|
- **🖼️ Мульти-медиа в одном посте**: Удобная карусель с индикатором `1 / 4` для постов с несколькими файлами.
|
||||||
- **⭐ Любимые авторы сверху**: Верхняя панель со «сториз»-аватарами любимых создателей и мгновенным просмотром их свежих фото и видео.
|
- **⭐ Любимые авторы**: Компактная кнопка-звёздочка сверху справа для быстрого доступа ко всем любимым авторам и их свежему медиапотоку.
|
||||||
|
- **📜 Бесконечная подгрузка постов автора**: Умная пагинация при прокрутке вниз загружает всю историю работ без ограничений.
|
||||||
- **🐾 Поддержка Pawchive**: Интеграция с `pawchive.pw` для просмотра профилей авторов, связанных аккаунтов, тегов и работ с Patreon, Fanbox, Fantia, Discord, Boosty и др.
|
- **🐾 Поддержка Pawchive**: Интеграция с `pawchive.pw` для просмотра профилей авторов, связанных аккаунтов, тегов и работ с Patreon, Fanbox, Fantia, Discord, Boosty и др.
|
||||||
- **🔍 Умный поиск и автодополнение**: Интерактивные чипсы тегов с подсказками от активных API. Быстрое исключение (`-tag`), добавление (`+tag`) или отправка в черный список по долгому тапу.
|
- **🔍 Умный поиск и автодополнение**: Интерактивные чипсы тегов с подсказками от активных API. Быстрое исключение (`-tag`), добавление (`+tag`) или отправка в черный список по долгому тапу.
|
||||||
- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация по тегам, категориям, рейтингу контента (safe/questionable/explicit), провайдерам и минимальному score.
|
- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация по тегам, категориям, рейтингу контента (safe/questionable/explicit), провайдерам и минимальному score.
|
||||||
@@ -52,9 +53,9 @@
|
|||||||
|
|
||||||
🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
|
🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
|
||||||
|
|
||||||
- **Android**: `Lunaris-v2.0.2.apk`
|
- **Android**: `Lunaris-v2.0.3.apk`
|
||||||
- **Linux (x64)**: `Lunaris-v2.0.2-linux-x64.tar.gz`
|
- **Linux (x64)**: `Lunaris-v2.0.3-linux-x64.tar.gz`
|
||||||
- **Windows**: `LunarisSetup-v2.0.2.exe` / `LunarisPortable-v2.0.2.zip`
|
- **Windows**: `LunarisSetup-v2.0.3.exe` / `LunarisPortable-v2.0.3.zip`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
const appDisplayVersion = '2.0.2';
|
const appDisplayVersion = '2.0.3';
|
||||||
const appBuildNumber = 24;
|
const appBuildNumber = 25;
|
||||||
const appReleaseChannel = 'Stable';
|
const appReleaseChannel = 'Stable';
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
const lunarisChangelog = [
|
const lunarisChangelog = [
|
||||||
|
LunarisChange(
|
||||||
|
version: '2.0.3',
|
||||||
|
title: 'Infinite artist pagination, favorite artists modal, and card layout polish',
|
||||||
|
bullets: [
|
||||||
|
'Implemented infinite scroll pagination in the artist post feed — now loads all historical works endlessly as you scroll.',
|
||||||
|
'Moved favorite artists to a compact star action button in the top-right header that opens an interactive modal.',
|
||||||
|
'Expanded artist card vertical height so updated dates and statistics are never cut off.',
|
||||||
|
'Removed mapping truncation in Pawchive provider to deliver all post attachments cleanly.',
|
||||||
|
],
|
||||||
|
),
|
||||||
LunarisChange(
|
LunarisChange(
|
||||||
version: '2.0.2',
|
version: '2.0.2',
|
||||||
title: 'Swipeable media pager, favorite artists, performance and Pawchive',
|
title: 'Swipeable media pager, favorite artists, performance and Pawchive',
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ class PawchiveProvider
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
mapped.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
mapped.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
return mapped.take(limit).toList(growable: false);
|
return mapped.toList(growable: false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'$name artist posts are unavailable right now. ${_shortError(error)}',
|
'$name artist posts are unavailable right now. ${_shortError(error)}',
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ final artistPostsProvider =
|
|||||||
final provider = providers.data.firstWhere(
|
final provider = providers.data.firstWhere(
|
||||||
(item) => (item as ContentProvider).id == query.providerId,
|
(item) => (item as ContentProvider).id == query.providerId,
|
||||||
);
|
);
|
||||||
final posts = await provider.getArtistPosts(query: query, limit: 50);
|
final posts = await provider.getArtistPosts(query: query, page: 0, limit: 50);
|
||||||
await ref.read(cacheServiceProvider).cachePosts(posts);
|
await ref.read(cacheServiceProvider).cachePosts(posts);
|
||||||
return posts;
|
return posts;
|
||||||
});
|
});
|
||||||
|
|
||||||
class ArtistPostsScreen extends ConsumerWidget {
|
class ArtistPostsScreen extends ConsumerStatefulWidget {
|
||||||
const ArtistPostsScreen({
|
const ArtistPostsScreen({
|
||||||
required this.providerId,
|
required this.providerId,
|
||||||
required this.service,
|
required this.service,
|
||||||
@@ -40,77 +40,172 @@ class ArtistPostsScreen extends ConsumerWidget {
|
|||||||
final String artistName;
|
final String artistName;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<ArtistPostsScreen> createState() => _ArtistPostsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
final List<Post> _posts = [];
|
||||||
|
int _page = 0;
|
||||||
|
bool _loading = false;
|
||||||
|
bool _hasMore = true;
|
||||||
|
Object? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_scrollController.addListener(() {
|
||||||
|
if (_scrollController.position.extentAfter < 600) {
|
||||||
|
_loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _loadMore());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refresh() async {
|
||||||
|
setState(() {
|
||||||
|
_posts.clear();
|
||||||
|
_page = 0;
|
||||||
|
_hasMore = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
await _loadMore();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMore() async {
|
||||||
|
if (_loading || !_hasMore) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_error = null;
|
||||||
|
});
|
||||||
|
|
||||||
final query = ArtistWorkQuery(
|
final query = ArtistWorkQuery(
|
||||||
providerId: providerId,
|
providerId: widget.providerId,
|
||||||
service: service,
|
service: widget.service,
|
||||||
artistId: artistId,
|
artistId: widget.artistId,
|
||||||
artistName: artistName,
|
artistName: widget.artistName,
|
||||||
);
|
);
|
||||||
final posts = ref.watch(artistPostsProvider(query));
|
|
||||||
|
try {
|
||||||
|
final providers =
|
||||||
|
await ref.read(providerManagerProvider).activeArtistProviders();
|
||||||
|
if (providers is! Success<List<ArtistProvider>>) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = 'Could not load artist providers';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final provider = providers.data.firstWhere(
|
||||||
|
(item) => (item as ContentProvider).id == query.providerId,
|
||||||
|
);
|
||||||
|
final newPosts = await provider.getArtistPosts(
|
||||||
|
query: query,
|
||||||
|
page: _page,
|
||||||
|
limit: 50,
|
||||||
|
);
|
||||||
|
await ref.read(cacheServiceProvider).cachePosts(newPosts);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
if (newPosts.isEmpty) {
|
||||||
|
_hasMore = false;
|
||||||
|
} else {
|
||||||
|
final existingKeys = _posts.map((p) => p.cacheKey).toSet();
|
||||||
|
final uniqueNew =
|
||||||
|
newPosts.where((p) => !existingKeys.contains(p.cacheKey)).toList();
|
||||||
|
_posts.addAll(uniqueNew);
|
||||||
|
_page++;
|
||||||
|
if (newPosts.length < 10) {
|
||||||
|
_hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
_error = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final settings =
|
final settings =
|
||||||
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
|
||||||
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
|
final favoriteKeys = ref.watch(favoriteKeysProvider).value ?? <String>{};
|
||||||
final viewedKeys = ref.watch(viewedKeysProvider).value ?? <String>{};
|
final viewedKeys = ref.watch(viewedKeysProvider).value ?? <String>{};
|
||||||
|
|
||||||
return AdaptiveScaffold(
|
return AdaptiveScaffold(
|
||||||
title: artistName,
|
title: widget.artistName,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Refresh',
|
||||||
onPressed: () => ref.invalidate(artistPostsProvider(query)),
|
onPressed: _refresh,
|
||||||
icon: const Icon(Icons.refresh_rounded),
|
icon: const Icon(Icons.refresh_rounded),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
body: posts.when(
|
body: _posts.isEmpty && _loading
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
? const Center(child: CircularProgressIndicator())
|
||||||
error: (error, _) => ErrorView(
|
: _posts.isEmpty && _error != null
|
||||||
message: _friendlyArtistPostError(error),
|
? ErrorView(
|
||||||
onRetry: () => ref.invalidate(artistPostsProvider(query)),
|
message: _friendlyArtistPostError(_error!),
|
||||||
),
|
onRetry: _refresh,
|
||||||
data: (items) => items.isEmpty
|
)
|
||||||
? const EmptyView(
|
: _posts.isEmpty
|
||||||
title: 'No works',
|
? const EmptyView(
|
||||||
message: 'This artist has no visible media yet.',
|
title: 'No works',
|
||||||
)
|
message: 'This artist has no visible media yet.',
|
||||||
: PostMasonryGrid(
|
)
|
||||||
posts: items,
|
: PostMasonryGrid(
|
||||||
columns: Responsive.columnsFor(
|
posts: _posts,
|
||||||
context,
|
controller: _scrollController,
|
||||||
mobileColumns: settings.mobileColumns,
|
loading: _loading,
|
||||||
desktopColumns: settings.desktopColumns,
|
columns: Responsive.columnsFor(
|
||||||
),
|
context,
|
||||||
blurExplicit: settings.blurExplicitContent,
|
mobileColumns: settings.mobileColumns,
|
||||||
showBadges: settings.showPostBadges,
|
desktopColumns: settings.desktopColumns,
|
||||||
nsfwEnabled: settings.nsfwEnabled,
|
),
|
||||||
mediaQualityMode:
|
blurExplicit: settings.blurExplicitContent,
|
||||||
MediaQualityMode.fromName(settings.mediaQualityMode),
|
showBadges: settings.showPostBadges,
|
||||||
favoriteKeys: favoriteKeys,
|
nsfwEnabled: settings.nsfwEnabled,
|
||||||
viewedKeys: viewedKeys,
|
mediaQualityMode:
|
||||||
onOpen: (post) => context.push(
|
MediaQualityMode.fromName(settings.mediaQualityMode),
|
||||||
'/post/${post.providerId}/${post.id}',
|
favoriteKeys: favoriteKeys,
|
||||||
extra: PostNavigationContext(
|
viewedKeys: viewedKeys,
|
||||||
currentPost: post,
|
onOpen: (post) => context.push(
|
||||||
posts: items,
|
'/post/${post.providerId}/${post.id}',
|
||||||
),
|
extra: PostNavigationContext(
|
||||||
),
|
currentPost: post,
|
||||||
onFavorite: (post) async {
|
posts: _posts,
|
||||||
if (favoriteKeys.contains(post.cacheKey)) {
|
),
|
||||||
await ref
|
),
|
||||||
.read(favoriteServiceProvider)
|
onFavorite: (post) async {
|
||||||
.removeFavorite(post.id, post.providerId);
|
if (favoriteKeys.contains(post.cacheKey)) {
|
||||||
} else {
|
await ref
|
||||||
await ref.read(favoriteServiceProvider).addFavorite(
|
.read(favoriteServiceProvider)
|
||||||
post,
|
.removeFavorite(post.id, post.providerId);
|
||||||
settings: settings,
|
} else {
|
||||||
downloadManager:
|
await ref.read(favoriteServiceProvider).addFavorite(
|
||||||
ref.read(downloadManagerServiceProvider),
|
post,
|
||||||
);
|
settings: settings,
|
||||||
}
|
downloadManager:
|
||||||
ref.invalidate(favoriteKeysProvider);
|
ref.read(downloadManagerServiceProvider),
|
||||||
ref.invalidate(favoritesControllerProvider);
|
);
|
||||||
},
|
}
|
||||||
),
|
ref.invalidate(favoriteKeysProvider);
|
||||||
),
|
ref.invalidate(favoritesControllerProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -107,16 +107,23 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
|
|||||||
.whereType<_FavoriteArtistItem>()
|
.whereType<_FavoriteArtistItem>()
|
||||||
.toList();
|
.toList();
|
||||||
final favoriteKeys = favoriteItems.map((e) => e.key).toSet();
|
final favoriteKeys = favoriteItems.map((e) => e.key).toSet();
|
||||||
final selectedFav = favoriteItems.firstWhere(
|
|
||||||
(e) => e.key == _selectedFavoriteArtistKey,
|
|
||||||
orElse: () => favoriteItems.isNotEmpty
|
|
||||||
? favoriteItems.first
|
|
||||||
: const _FavoriteArtistItem.empty(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return AdaptiveScaffold(
|
return AdaptiveScaffold(
|
||||||
title: 'Artists',
|
title: 'Artists',
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Любимые авторы',
|
||||||
|
icon: Badge(
|
||||||
|
isLabelVisible: favoriteItems.isNotEmpty,
|
||||||
|
label: Text('${favoriteItems.length}'),
|
||||||
|
child: const Icon(Icons.star_rounded, color: Colors.amber),
|
||||||
|
),
|
||||||
|
onPressed: () => _showFavoriteArtistsModal(
|
||||||
|
context,
|
||||||
|
settings,
|
||||||
|
favoriteItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Refresh',
|
tooltip: 'Refresh',
|
||||||
onPressed: () => _refresh(configs.valueOrNull ?? const []),
|
onPressed: () => _refresh(configs.valueOrNull ?? const []),
|
||||||
@@ -150,32 +157,6 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
|
|||||||
},
|
},
|
||||||
onSearch: () => _refresh(items),
|
onSearch: () => _refresh(items),
|
||||||
),
|
),
|
||||||
_FavoriteArtistsSection(
|
|
||||||
favorites: favoriteItems,
|
|
||||||
selectedKey: selectedFav.isEmpty ? null : selectedFav.key,
|
|
||||||
onSelect: (fav) =>
|
|
||||||
setState(() => _selectedFavoriteArtistKey = fav.key),
|
|
||||||
onOpenArtist: (fav) => context.push(
|
|
||||||
'/artists/${fav.providerId}/${fav.service}/${fav.id}?name=${Uri.encodeComponent(fav.name)}',
|
|
||||||
),
|
|
||||||
onRemoveFavorite: (fav) async {
|
|
||||||
final updated = List<String>.from(settings.favoriteArtists)
|
|
||||||
..removeWhere((e) {
|
|
||||||
try {
|
|
||||||
return _FavoriteArtistItem.fromJson(
|
|
||||||
jsonDecode(e) as Map<String, dynamic>)
|
|
||||||
.key ==
|
|
||||||
fav.key;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await ref
|
|
||||||
.read(settingsControllerProvider.notifier)
|
|
||||||
.saveSettings(
|
|
||||||
settings.copyWith(favoriteArtists: updated));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _error != null
|
child: _error != null
|
||||||
? ErrorView(
|
? ErrorView(
|
||||||
@@ -206,7 +187,7 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
|
|||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: 12,
|
||||||
mainAxisExtent:
|
mainAxisExtent:
|
||||||
Responsive.isMobile(context) ? 88 : 104,
|
Responsive.isMobile(context) ? 104 : 116,
|
||||||
),
|
),
|
||||||
itemCount: _artists.length + (_loading ? 1 : 0),
|
itemCount: _artists.length + (_loading ? 1 : 0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -285,6 +266,50 @@ class _ArtistsScreenState extends ConsumerState<ArtistsScreen> {
|
|||||||
if (mounted) setState(() => _loading = false);
|
if (mounted) setState(() => _loading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showFavoriteArtistsModal(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettings settings,
|
||||||
|
List<_FavoriteArtistItem> favorites,
|
||||||
|
) {
|
||||||
|
showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (ctx) {
|
||||||
|
return _FavoriteArtistsModal(
|
||||||
|
favorites: favorites,
|
||||||
|
initialSelectedKey: _selectedFavoriteArtistKey,
|
||||||
|
onSelect: (fav) =>
|
||||||
|
setState(() => _selectedFavoriteArtistKey = fav.key),
|
||||||
|
onOpenArtist: (fav) => context.push(
|
||||||
|
'/artists/${fav.providerId}/${fav.service}/${fav.id}?name=${Uri.encodeComponent(fav.name)}',
|
||||||
|
),
|
||||||
|
onRemoveFavorite: (fav) async {
|
||||||
|
final updated = List<String>.from(settings.favoriteArtists)
|
||||||
|
..removeWhere((e) {
|
||||||
|
try {
|
||||||
|
return _FavoriteArtistItem.fromJson(
|
||||||
|
jsonDecode(e) as Map<String, dynamic>)
|
||||||
|
.key ==
|
||||||
|
fav.key;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await ref
|
||||||
|
.read(settingsControllerProvider.notifier)
|
||||||
|
.saveSettings(settings.copyWith(favoriteArtists: updated));
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ArtistsHeader extends StatelessWidget {
|
class _ArtistsHeader extends StatelessWidget {
|
||||||
@@ -388,7 +413,7 @@ class _ArtistCard extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_ArtistAvatar(artist: artist),
|
_ArtistAvatar(artist: artist),
|
||||||
@@ -492,6 +517,176 @@ class _FavoriteArtistItem {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FavoriteArtistsModal extends StatefulWidget {
|
||||||
|
const _FavoriteArtistsModal({
|
||||||
|
required this.favorites,
|
||||||
|
required this.initialSelectedKey,
|
||||||
|
required this.onSelect,
|
||||||
|
required this.onOpenArtist,
|
||||||
|
required this.onRemoveFavorite,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<_FavoriteArtistItem> favorites;
|
||||||
|
final String? initialSelectedKey;
|
||||||
|
final ValueChanged<_FavoriteArtistItem> onSelect;
|
||||||
|
final ValueChanged<_FavoriteArtistItem> onOpenArtist;
|
||||||
|
final ValueChanged<_FavoriteArtistItem> onRemoveFavorite;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_FavoriteArtistsModal> createState() => _FavoriteArtistsModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FavoriteArtistsModalState extends State<_FavoriteArtistsModal> {
|
||||||
|
late String? _selectedKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedKey = widget.initialSelectedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final selectedFav = widget.favorites.firstWhere(
|
||||||
|
(e) => e.key == _selectedKey,
|
||||||
|
orElse: () => widget.favorites.isNotEmpty
|
||||||
|
? widget.favorites.first
|
||||||
|
: const _FavoriteArtistItem.empty(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
expand: false,
|
||||||
|
initialChildSize: widget.favorites.isEmpty ? 0.35 : 0.65,
|
||||||
|
minChildSize: 0.25,
|
||||||
|
maxChildSize: 0.92,
|
||||||
|
builder: (context, scroll) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
controller: scroll,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: scheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.star_rounded,
|
||||||
|
color: Colors.amber, size: 26),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Любимые авторы',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.favorites.isNotEmpty) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${widget.favorites.length}',
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||||
|
color: Colors.amber.shade900,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close_rounded),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
if (widget.favorites.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 36, 24, 40),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.star_border_rounded,
|
||||||
|
size: 52,
|
||||||
|
color: scheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Список пуст',
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Нажмите на иконку звёздочки ★ на карточке любого автора, чтобы добавить его в избранное и быстро смотреть свежие работы.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: scheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
_FavoriteArtistsSection(
|
||||||
|
favorites: widget.favorites,
|
||||||
|
selectedKey: selectedFav.isEmpty ? null : selectedFav.key,
|
||||||
|
onSelect: (fav) {
|
||||||
|
setState(() => _selectedKey = fav.key);
|
||||||
|
widget.onSelect(fav);
|
||||||
|
},
|
||||||
|
onOpenArtist: (fav) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onOpenArtist(fav);
|
||||||
|
},
|
||||||
|
onRemoveFavorite: (fav) {
|
||||||
|
widget.onRemoveFavorite(fav);
|
||||||
|
if (_selectedKey == fav.key) {
|
||||||
|
setState(() {
|
||||||
|
_selectedKey = widget.favorites
|
||||||
|
.where((e) => e.key != fav.key)
|
||||||
|
.firstOrNull
|
||||||
|
?.key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _FavoriteArtistsSection extends ConsumerWidget {
|
class _FavoriteArtistsSection extends ConsumerWidget {
|
||||||
const _FavoriteArtistsSection({
|
const _FavoriteArtistsSection({
|
||||||
required this.favorites,
|
required this.favorites,
|
||||||
|
|||||||
+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: 2.0.2+24
|
version: 2.0.3+25
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user