From b82d87161ebe502862715d6078e04d503ceefd07 Mon Sep 17 00:00:00 2001
From: RarDog
Date: Thu, 3 Sep 2026 20:43:38 +0300
Subject: [PATCH] feat: release v2.0.6 with full Pawchive API integration
(global feed, tags, links, announcements, server media filter)
---
README.md | 35 +-
lib/app/app_version.dart | 4 +-
lib/app/changelog.dart | 11 +
lib/backend/backend.dart | 3 +
lib/backend/models/artist_announcement.dart | 35 ++
lib/backend/models/artist_link.dart | 39 +++
lib/backend/models/artist_tag.dart | 21 ++
lib/backend/models/artist_work_query.dart | 35 +-
lib/backend/providers/content_provider.dart | 18 +-
lib/backend/providers/kemono_provider.dart | 18 +
lib/backend/providers/pawchive_provider.dart | 106 +++++-
.../presentation/artist_posts_screen.dart | 316 +++++++++++++++---
pubspec.yaml | 2 +-
test/backend/pawchive_features_test.dart | 76 +++++
14 files changed, 652 insertions(+), 67 deletions(-)
create mode 100644 lib/backend/models/artist_announcement.dart
create mode 100644 lib/backend/models/artist_link.dart
create mode 100644 lib/backend/models/artist_tag.dart
create mode 100644 test/backend/pawchive_features_test.dart
diff --git a/README.md b/README.md
index 3d3c0d5..56a54d3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
@@ -20,26 +20,29 @@
## ✨ Основные возможности
+- **🌐 Общая лента Pawchive**: Недавние посты со всех платформ прямо в главной ленте (Feed) с поддержкой поиска.
+- **🏷️ Интерактивные теги автора**: Быстрая фильтрация по темам с живыми счётчиками постов (`#tag (count)`).
+- **📢 Анонсы и новости авторов**: Всплывающее окно анонсов автора с датами и текстом обращений к аудитории.
+- **🔗 Связанные аккаунты**: Удобный переход между профилями автора на разных платформах (Patreon, Fanbox, Discord и др.).
+- **🎬 Серверная фильтрация по типу медиа**: Поиск фото, видео и GIF по всей глубине архива автора через API.
- **📱 Плавная лента в стиле Pinterest**: Masonry-сетка с переключением режима (Masonry, 1:1 Сетка, Список) в шапке ленты.
- **⚡ Сверхбыстрые видео-превью**: Мгновенная загрузка превью без фризов и лишней нагрузки на процессор и память смартфона.
-- **↔️ Листание постов влево и вправо**: Полноэкранный пейджер с плавными свайпами между фото и видео во всех разделах (Лента, Авторы, Избранное, Коллекции, История просмотров).
+- **↔️ Листание постов влево и вправо**: Полноэкранный пейджер с плавными свайпами между фото и видео во всех разделах.
- **❤️ Двойной тап для лайка и свайп вниз для закрытия**: Анимированное сердечко по двойному тапу и закрытие экрана просмотра свайпом вниз.
- **📤 Прямая отправка файла (Share)**: Мгновенный экспорт оригинальных медиафайлов через системное меню «Поделиться».
-- **🔖 Сохраненные поисковые пресеты**: Быстрое сохранение комбинаций тегов и фильтров в закладки с мгновенным переключением в один клик.
+- **🔖 Сохраненные поисковые пресеты**: Быстрое сохранение комбинаций тегов и фильтров в закладки.
- **📁 Умная структура папок скачивания**: Настраиваемые шаблоны поддиректорий (`{Artist}`, `{Provider}`, `{Service}`, `{ID}`, `{Date}`).
-- **🗑️ Менеджер кэша**: Детальная визуализация занятой памяти (база данных, ОЗУ, временные файлы) и выборочная очистка.
+- **🗑️ Менеджер кэша**: Детальная визуализация занятой памяти и выборочная очистка.
- **📦 Бэкап и синхронизация**: Экспорт и импорт полной конфигурации в JSON-файл для переноса на другие устройства.
- **🌑 AMOLED Pure Black тема**: Истинно черный фон (`#000000`) для экономии батареи на OLED дисплеях.
- **🎨 Динамические цвета Material You**: Поддержка системной палитры Android из обоев устройства.
-- **⭐ Любимые авторы**: Компактная кнопка-звёздочка сверху справа для быстрого доступа ко всем любимым авторам и их свежему медиапотоку.
-- **🏷️ Мультивыбор платформ**: Быстрый выбор одной или нескольких платформ авторов (Patreon, Fanbox, Fantia, Boosty, Discord).
-- **🎬 Фильтр фото, видео и GIF у автора**: Удобные чипсы с мультивыбором типа контента и количеством найденных медиа.
-- **📜 Бесконечная подгрузка постов автора**: Умная пагинация при прокрутке вниз загружает всю историю работ без ограничений.
-- **🐾 Поддержка Pawchive**: Интеграция с `pawchive.pw` для просмотра профилей авторов, связанных аккаунтов, тегов и работ.
-- **🔍 Умный поиск и автодополнение**: Интерактивные чипсы тегов с подсказками от активных API. Быстрое исключение (`-tag`), добавление (`+tag`) или отправка в черный список по долгому тапу.
-- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация по тегам, категориям, рейтингу контента (safe/questionable/explicit), провайдерам и минимальному score.
-- **🎬 Встроенный плеер**: Удобное воспроизведение WebM / MP4, запоминание настроек звука, fullscreen и повтор.
-- **💾 Менеджер загрузок и коллекции**: Скачивание оригиналов в высоком качестве, тематические папки-коллекции и автозагрузка избранного.
+- **⭐ Любимые авторы**: Компактная кнопка-звёздочка сверху справа для быстрого доступа ко всем любимым авторам.
+- **🏷️ Мультивыбор платформ**: Выбор нескольких платформ (Patreon, Fanbox, Fantia, Boosty, Discord).
+- **📜 Бесконечная подгрузка постов автора**: Умная пагинация при прокрутке вниз загружает всю историю работ.
+- **🔍 Умный поиск и автодополнение**: Интерактивные чипсы тегов с подсказками от активных API.
+- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация по тегам, категориям, рейтингу контента и минимальному score.
+- **🎬 Встроенный плеер**: Удобное воспроизведение WebM / MP4, запоминание звука, fullscreen и повтор.
+- **💾 Менеджер загрузок и коллекции**: Скачивание оригиналов в высоком качестве и тематические папки-коллекции.
- **🔄 Встроенное автообновление**: Проверка и скачивание свежих релизов напрямую из личного репозитория Gitea.
---
@@ -62,9 +65,9 @@
🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
-- **Android**: `Lunaris-v2.0.5.apk`
-- **Linux (x64)**: `Lunaris-v2.0.5-linux-x64.tar.gz`
-- **Windows**: `LunarisSetup-v2.0.5.exe` / `LunarisPortable-v2.0.5.zip`
+- **Android**: `Lunaris-v2.0.6.apk`
+- **Linux (x64)**: `Lunaris-v2.0.6-linux-x64.tar.gz`
+- **Windows**: `LunarisSetup-v2.0.6.exe` / `LunarisPortable-v2.0.6.zip`
---
diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart
index 84ac377..d406262 100644
--- a/lib/app/app_version.dart
+++ b/lib/app/app_version.dart
@@ -1,3 +1,3 @@
-const appDisplayVersion = '2.0.5';
-const appBuildNumber = 27;
+const appDisplayVersion = '2.0.6';
+const appBuildNumber = 28;
const appReleaseChannel = 'Stable';
diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart
index 95a05ca..f48f5d0 100644
--- a/lib/app/changelog.dart
+++ b/lib/app/changelog.dart
@@ -1,4 +1,15 @@
const lunarisChangelog = [
+ LunarisChange(
+ version: '2.0.6',
+ title: 'Pawchive Deep Integration: Global Posts Feed, Artist Tags, Announcements, Links & Server Media Filter',
+ bullets: [
+ 'Connected Pawchive global recent posts (/api/v1/posts) to the main Feed screen with search.',
+ 'Interactive artist tags bar (/tags) with live post counts (#tag (N)) and instant topic filtering.',
+ 'Linked accounts bar (/links) with direct navigation to author profiles across platforms.',
+ 'Artist announcements modal (/announcements) showing creator news, dates and status updates.',
+ 'Server-side media filtering (Photos, Videos, GIFs) querying the entire author archive through the API.',
+ ],
+ ),
LunarisChange(
version: '2.0.5',
title: '10 QoL Super-Update: AMOLED, Dynamic Colors, Grid switch, Share, Presets, Cache & Backup',
diff --git a/lib/backend/backend.dart b/lib/backend/backend.dart
index 0350a2d..409dacb 100644
--- a/lib/backend/backend.dart
+++ b/lib/backend/backend.dart
@@ -4,6 +4,9 @@ export 'models/content_provider_config.dart';
export 'models/app_update_info.dart';
export 'models/artist_profile.dart';
export 'models/artist_work_query.dart';
+export 'models/artist_tag.dart';
+export 'models/artist_link.dart';
+export 'models/artist_announcement.dart';
export 'models/download_task.dart';
export 'models/downloaded_media.dart';
export 'models/favorite.dart';
diff --git a/lib/backend/models/artist_announcement.dart b/lib/backend/models/artist_announcement.dart
new file mode 100644
index 0000000..0f8bb69
--- /dev/null
+++ b/lib/backend/models/artist_announcement.dart
@@ -0,0 +1,35 @@
+class ArtistAnnouncement {
+ const ArtistAnnouncement({
+ required this.service,
+ required this.userId,
+ required this.content,
+ this.hash,
+ this.added,
+ });
+
+ final String service;
+ final String userId;
+ final String content;
+ final String? hash;
+ final DateTime? added;
+
+ factory ArtistAnnouncement.fromJson(Map json) {
+ return ArtistAnnouncement(
+ service: (json['service'] ?? '').toString(),
+ userId: (json['user_id'] ?? '').toString(),
+ content: (json['content'] ?? '').toString(),
+ hash: json['hash']?.toString(),
+ added: json['added'] != null
+ ? DateTime.tryParse(json['added'].toString())
+ : null,
+ );
+ }
+
+ Map toJson() => {
+ 'service': service,
+ 'user_id': userId,
+ 'content': content,
+ if (hash != null) 'hash': hash,
+ if (added != null) 'added': added?.toIso8601String(),
+ };
+}
diff --git a/lib/backend/models/artist_link.dart b/lib/backend/models/artist_link.dart
new file mode 100644
index 0000000..bc7c419
--- /dev/null
+++ b/lib/backend/models/artist_link.dart
@@ -0,0 +1,39 @@
+class ArtistLink {
+ const ArtistLink({
+ required this.id,
+ required this.service,
+ required this.name,
+ this.publicId,
+ this.indexed,
+ this.updated,
+ });
+
+ final String id;
+ final String service;
+ final String name;
+ final String? publicId;
+ final DateTime? indexed;
+ final DateTime? updated;
+
+ factory ArtistLink.fromJson(Map json) {
+ return ArtistLink(
+ id: (json['id'] ?? '').toString(),
+ service: (json['service'] ?? '').toString(),
+ name: (json['name'] ?? '').toString(),
+ publicId: json['public_id']?.toString(),
+ indexed: json['indexed'] != null
+ ? DateTime.tryParse(json['indexed'].toString())
+ : null,
+ updated: json['updated'] != null
+ ? DateTime.tryParse(json['updated'].toString())
+ : null,
+ );
+ }
+
+ Map toJson() => {
+ 'id': id,
+ 'service': service,
+ 'name': name,
+ if (publicId != null) 'public_id': publicId,
+ };
+}
diff --git a/lib/backend/models/artist_tag.dart b/lib/backend/models/artist_tag.dart
new file mode 100644
index 0000000..7efb0c7
--- /dev/null
+++ b/lib/backend/models/artist_tag.dart
@@ -0,0 +1,21 @@
+class ArtistTag {
+ const ArtistTag({
+ required this.tag,
+ required this.postCount,
+ });
+
+ final String tag;
+ final int postCount;
+
+ factory ArtistTag.fromJson(Map json) {
+ return ArtistTag(
+ tag: (json['tag'] ?? '').toString(),
+ postCount: (json['post_count'] as num?)?.toInt() ?? 0,
+ );
+ }
+
+ Map toJson() => {
+ 'tag': tag,
+ 'post_count': postCount,
+ };
+}
diff --git a/lib/backend/models/artist_work_query.dart b/lib/backend/models/artist_work_query.dart
index 495e4d7..90534a5 100644
--- a/lib/backend/models/artist_work_query.dart
+++ b/lib/backend/models/artist_work_query.dart
@@ -4,12 +4,34 @@ class ArtistWorkQuery {
required this.service,
required this.artistId,
required this.artistName,
+ this.queryText,
+ this.tagFilter,
});
final String providerId;
final String service;
final String artistId;
final String artistName;
+ final String? queryText;
+ final String? tagFilter;
+
+ ArtistWorkQuery copyWith({
+ String? providerId,
+ String? service,
+ String? artistId,
+ String? artistName,
+ String? queryText,
+ String? tagFilter,
+ }) {
+ return ArtistWorkQuery(
+ providerId: providerId ?? this.providerId,
+ service: service ?? this.service,
+ artistId: artistId ?? this.artistId,
+ artistName: artistName ?? this.artistName,
+ queryText: queryText ?? this.queryText,
+ tagFilter: tagFilter ?? this.tagFilter,
+ );
+ }
@override
bool operator ==(Object other) {
@@ -17,9 +39,18 @@ class ArtistWorkQuery {
other.providerId == providerId &&
other.service == service &&
other.artistId == artistId &&
- other.artistName == artistName;
+ other.artistName == artistName &&
+ other.queryText == queryText &&
+ other.tagFilter == tagFilter;
}
@override
- int get hashCode => Object.hash(providerId, service, artistId, artistName);
+ int get hashCode => Object.hash(
+ providerId,
+ service,
+ artistId,
+ artistName,
+ queryText,
+ tagFilter,
+ );
}
diff --git a/lib/backend/providers/content_provider.dart b/lib/backend/providers/content_provider.dart
index 375ee15..ef0aba7 100644
--- a/lib/backend/providers/content_provider.dart
+++ b/lib/backend/providers/content_provider.dart
@@ -1,10 +1,13 @@
+import '../models/artist_announcement.dart';
+import '../models/artist_link.dart';
+import '../models/artist_profile.dart';
+import '../models/artist_tag.dart';
+import '../models/artist_work_query.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/tag_suggestion.dart';
import '../models/top_period_filter.dart';
-import '../models/artist_profile.dart';
-import '../models/artist_work_query.dart';
abstract class ContentProvider {
String get id;
@@ -72,4 +75,15 @@ abstract class ArtistProvider {
String artistId,
String postId,
);
+
+ Future> getArtistTags(String service, String artistId) async =>
+ const [];
+
+ Future> getArtistLinks(
+ String service, String artistId) async =>
+ const [];
+
+ Future> getArtistAnnouncements(
+ String service, String artistId) async =>
+ const [];
}
diff --git a/lib/backend/providers/kemono_provider.dart b/lib/backend/providers/kemono_provider.dart
index 57f3fce..b617614 100644
--- a/lib/backend/providers/kemono_provider.dart
+++ b/lib/backend/providers/kemono_provider.dart
@@ -1,7 +1,10 @@
import 'package:dio/dio.dart';
import '../../core/http/dio_client.dart';
+import '../models/artist_announcement.dart';
+import '../models/artist_link.dart';
import '../models/artist_profile.dart';
+import '../models/artist_tag.dart';
import '../models/artist_work_query.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
@@ -214,6 +217,21 @@ class KemonoProvider
return getArtistPostComments(parts[0], parts[1], parts[2]);
}
+ @override
+ Future> getArtistTags(
+ String service, String artistId) async =>
+ const [];
+
+ @override
+ Future> getArtistLinks(
+ String service, String artistId) async =>
+ const [];
+
+ @override
+ Future> getArtistAnnouncements(
+ String service, String artistId) async =>
+ const [];
+
String _creatorPath() => _isCoomer ? '/coomer' : '/kemono';
List _artistsFromResponse(dynamic data) {
diff --git a/lib/backend/providers/pawchive_provider.dart b/lib/backend/providers/pawchive_provider.dart
index 774201d..cdab8bc 100644
--- a/lib/backend/providers/pawchive_provider.dart
+++ b/lib/backend/providers/pawchive_provider.dart
@@ -3,7 +3,10 @@ import 'dart:async';
import 'package:dio/dio.dart';
import '../../core/http/dio_client.dart';
+import '../models/artist_announcement.dart';
+import '../models/artist_link.dart';
import '../models/artist_profile.dart';
+import '../models/artist_tag.dart';
import '../models/artist_work_query.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
@@ -42,7 +45,34 @@ class PawchiveProvider
String? rating,
TopPeriodFilter topPeriod = TopPeriodFilter.none,
}) async {
- return const [];
+ final offset = (page - 1).clamp(0, 999999) * 50;
+ final queryText = tags.join(' ').trim();
+ try {
+ final response = await _dio.get(
+ '/api/v1/posts',
+ queryParameters: {
+ if (queryText.isNotEmpty) 'q': queryText,
+ 'o': offset,
+ },
+ );
+ final posts = _postItems(response.data);
+ final mapped = [];
+ for (final post in posts.whereType