diff --git a/.gitignore b/.gitignore
index dcb652d..372b9ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ dist/
.flutter-plugins-dependencies
android/local.properties
*.log
+*.apk
+*.tar.gz
diff --git a/README.md b/README.md
index 15ff7cf..5682597 100644
--- a/README.md
+++ b/README.md
@@ -1,104 +1,124 @@
-# Lunaris
+# 🌙 Lunaris
-Lunaris — локальное Flutter-приложение для просмотра booru, gelbooru, rule34-подобных источников и artist-архивов в Pinterest-стиле.
+
+
+
-Приложение не использует отдельный backend-сервер. Вся логика живет внутри Flutter: API-провайдеры, локальный кеш metadata, избранное, коллекции, история просмотров, фильтры, диагностика, загрузки и обновления.
+
+ Современный кроссплатформенный клиент для Booru и Creator-архивов
+ Плавный Pinterest-стиль, поддержка фото, видео и GIF, умный черный список, встроенный менеджер загрузок и автообновление.
+
-## Что умеет
+
+
+
+
+
+
-- Masonry feed с несколькими провайдерами и естественно перемешанной выдачей.
-- Поиск тегов через chip-input с подсказками из активных API.
-- Избранное, история просмотров, коллекции и пакетное добавление в коллекции.
-- Smart blacklist/whitelist по тегам, группам тегов, рейтингу, провайдеру, типу файла и score.
-- Details viewer для фото, GIF, видео и SWF на Windows.
-- Удобный видеоплеер с fullscreen, repeat, mute, half-volume и сохранением настроек.
-- Mobile image zoom: pinch-to-zoom и double tap в details.
-- Опциональное автоскачивание новых favorites с отдельным удалением локального файла без удаления из избранного.
-- Отдельная вкладка Artists для Kemono/Coomer без смешивания с обычной booru-лентой.
-- Управление провайдерами, health-check и provider diagnostics.
-- Кастомизация интерфейса: тема, язык RU/EN, акцентный цвет, видимые вкладки, beta/experimental обновления.
-- Экспорт и импорт настроек JSON.
-- Проверка Gitea Releases и скачивание APK/Windows installer прямо из приложения.
+---
-## Скачать
+## ✨ Основные возможности
-Готовые сборки лежат в Gitea Releases:
+- **📱 Плавная лента в стиле Pinterest**: Masonry-сетка с адаптивным количеством колонок для мобильных и ПК.
+- **⚡ Сверхбыстрые видео-превью**: Мгновенная загрузка превью без фризов и лишней нагрузки на процессор и память смартфона.
+- **↔️ Листание постов влево и вправо**: Полноэкранный пейджер с плавными свайпами между фото и видео во всех разделах (Лента, Авторы, Избранное, Коллекции, История просмотров).
+- **🖼️ Мульти-медиа в одном посте**: Удобная карусель с индикатором `1 / 4` для постов с несколькими файлами.
+- **⭐ Любимые авторы сверху**: Верхняя панель со «сториз»-аватарами любимых создателей и мгновенным просмотром их свежих фото и видео.
+- **🐾 Поддержка Pawchive**: Интеграция с `pawchive.pw` для просмотра профилей авторов, связанных аккаунтов, тегов и работ с Patreon, Fanbox, Fantia, Discord, Boosty и др.
+- **🔍 Умный поиск и автодополнение**: Интерактивные чипсы тегов с подсказками от активных API. Быстрое исключение (`-tag`), добавление (`+tag`) или отправка в черный список по долгому тапу.
+- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация по тегам, категориям, рейтингу контента (safe/questionable/explicit), провайдерам и минимальному score.
+- **🎬 Встроенный плеер**: Удобное воспроизведение WebM / MP4, запоминание настроек звука, fullscreen и повтор.
+- **💾 Менеджер загрузок и коллекции**: Скачивание оригиналов в высоком качестве, тематические папки-коллекции и автозагрузка избранного.
+- **🔄 Встроенное автообновление**: Проверка и скачивание свежих релизов напрямую из личного репозитория Gitea.
-[Lunaris 2.0.1](https://gitea.rardogsynapse.online/RarDog/RuleGelApp/releases/tag/v2.0.1)
+---
-- `Lunaris-v2.0.1.apk` — APK для Android.
-- `LunarisSetup-v2.0.1.exe` — установщик для Windows.
-- `LunarisPortable-v2.0.1.zip` — portable-версия для Windows.
+## 🌐 Активные провайдеры
-## Провайдеры
+| Провайдер | Тип API | Назначение |
+|---|---|---|
+| **Gelbooru** | XML / JSON API | Основная база аниме-артов |
+| **Rule34** | Booru API | Популярный booru-источник |
+| **Realbooru** | HTML Scraper | Фото и реалистичный контент |
+| **e621 / e926** | JSON API | Фурри- и антро-арт с богатой системой тегов |
+| **Pawchive** | REST API v1 | Архивы авторов Patreon, Pixiv Fanbox, Fantia и др. |
-Обычный feed:
+---
-- Gelbooru
-- Rule34
-- Safebooru
-- Konachan
-- Yande.re
-- e621 / e926
-- Xbooru
-- CosBooru
-- Realbooru отключен по умолчанию, если публичный API недоступен
+## 📥 Скачать приложение
-Artists flow:
+Готовые сборки для всех поддерживаемых платформ доступны на странице релизов:
-- Kemono
-- Coomer
+🔗 **[Gitea Releases (RarDog/Lunaris)](https://gitea.rardogsynapse.online/RarDog/Lunaris/releases)**
-Kemono/Coomer остаются отдельным разделом Artists и не попадают в общий booru feed.
+- **Android**: `Lunaris-v2.0.2.apk`
+- **Linux (x64)**: `Lunaris-v2.0.2-linux-x64.tar.gz`
+- **Windows**: `LunarisSetup-v2.0.2.exe` / `LunarisPortable-v2.0.2.zip`
-## Архитектура
+---
-- `lib/core` — HTTP, база, кеш, ошибки, Result и утилиты.
-- `lib/backend/models` — доменные модели.
-- `lib/backend/providers` — API-провайдеры и ProviderManager.
-- `lib/backend/repositories` — доступ к локальному Isar-хранилищу.
-- `lib/backend/services` — Feed/Search/Favorites/Collections/Settings/Diagnostics/Downloads/Updates.
-- `lib/backend/di` — Riverpod providers для backend/core.
-- `lib/app` — приложение, роутер, тема, локализация, motion и responsive helpers.
-- `lib/shared/widgets` — общие UI-компоненты.
-- `lib/features` — экраны и контроллеры фич.
+## 🛠️ Сборка из исходников
-## Запуск
+### Требования
+- Flutter SDK `>= 3.19.0`
+- OpenJDK 17
+- Android SDK (для сборки APK)
+- Clang / CMake / GTK-3 (для сборки под Linux)
+
+### Команды разработки
```bash
+# Клонирование репозитория
+git clone https://gitea.rardogsynapse.online/RarDog/Lunaris.git
+cd Lunaris
+
+# Установка зависимостей
flutter pub get
-flutter run -d windows
-flutter run -d android
-```
-## Сборка
-
-```bash
+# Запуск тестов и анализатора
flutter analyze
flutter test
-flutter build apk
-flutter build windows
+
+# Запуск на подключенном устройстве
+flutter run
```
-Windows installer собирается через Inno Setup:
+### Сборка релизов
-```powershell
-ISCC.exe installer\Lunaris.iss
+```bash
+# Android APK
+flutter build apk --release
+
+# Linux x64 Bundle
+flutter build linux --release
+
+# Windows Bundle
+flutter build windows --release
```
-## Как добавить провайдера
+---
-1. Создать класс, реализующий `ContentProvider`.
-2. Добавить mapper к единой модели `Post`.
-3. Зарегистрировать `apiType` в `ProviderFactory`.
-4. Добавить seed config в `ProviderRepository`, если провайдер нужен по умолчанию.
-5. Добавить тесты parser/provider factory.
+## 🏛️ Архитектура
-## Безопасность
+```
+lib/
+ ├── app/ # Роутер, тема, локализация (RU/EN), анимации
+ ├── backend/
+ │ ├── di/ # Riverpod providers для сервисов
+ │ ├── models/ # Post, ArtistProfile, AppSettings, AppUpdateInfo
+ │ ├── providers/ # Реализации API (Gelbooru, Rule34, Pawchive и др.)
+ │ ├── repositories/ # Доступ к локальной БД Isar
+ │ └── services/ # Бизнес-логика (Feed, Search, Favorites, Downloads, Updates)
+ ├── core/ # Сеть (Dio), база данных (Isar), ошибки и кеши
+ ├── features/ # Модули экранов (feed, post, artists, favorites, settings)
+ └── shared/widgets/ # Переиспользуемые виджеты (PostCard, MasonryGrid, TagChip)
+```
-- Feed работает только с metadata/preview и не скачивает original/full files автоматически.
-- Загрузка файлов происходит только по явному действию пользователя или при включенной настройке auto-download favorites.
-- Приложение уважает API и rate limits сайтов.
-- NSFW/blur/blacklist настраиваются локально.
-- API keys/custom headers хранятся локально и экспортируются только через явный export settings.
-- Локальные пути скачанных файлов не попадают в JSON export/import.
+---
+
+## 🔒 Приватность и безопасность
+
+- **Полная автономность**: Приложение не отправляет телеметрию и работает без внешних серверов-посредников.
+- **Локальное хранение**: Все настройки, история поиска, черный список и избранное хранятся исключительно на вашем устройстве в локальной базе Isar.
+- **Бережное отношение к трафику**: В ленте загружаются только оптимизированные превью. Тяжелые оригиналы и видео загружаются только при открытии или скачивании.
diff --git a/lib/app/app_version.dart b/lib/app/app_version.dart
index d0cb62d..dd09079 100644
--- a/lib/app/app_version.dart
+++ b/lib/app/app_version.dart
@@ -1,3 +1,3 @@
-const appDisplayVersion = '2.0.1';
-const appBuildNumber = 23;
+const appDisplayVersion = '2.0.2';
+const appBuildNumber = 24;
const appReleaseChannel = 'Stable';
diff --git a/lib/app/changelog.dart b/lib/app/changelog.dart
index 8c8fe20..94cdc78 100644
--- a/lib/app/changelog.dart
+++ b/lib/app/changelog.dart
@@ -1,4 +1,16 @@
const lunarisChangelog = [
+ LunarisChange(
+ version: '2.0.2',
+ title: 'Swipeable media pager, favorite artists, performance and Pawchive',
+ bullets: [
+ 'Full horizontal post pager across mobile and desktop in Feed, Artists, Favorites, Collections, and Viewed history.',
+ 'Eliminated video preview stutter and lag in grid feed by using high-performance cached thumbnails and on-demand playback.',
+ 'Added favorite artists bar at the top of the Artists tab with quick access and recent photos/videos stream.',
+ 'Multi-image posts now feature an in-post navigation indicator with image switching.',
+ 'New Pawchive provider support and cleaned up deprecated providers.',
+ 'In-app update checks and downloads connected directly to RarDog Gitea releases.',
+ ],
+ ),
LunarisChange(
version: '2.0.1',
title: 'Localization, zoom and favorite downloads',
diff --git a/lib/app/router.dart b/lib/app/router.dart
index 751ca49..f0aece9 100644
--- a/lib/app/router.dart
+++ b/lib/app/router.dart
@@ -40,11 +40,27 @@ final appRouterProvider = Provider((ref) {
),
GoRoute(
path: '/post/:providerId/:postId',
- builder: (context, state) => PostDetailsScreen(
- providerId: state.pathParameters['providerId']!,
- postId: state.pathParameters['postId']!,
- initialPost: state.extra is Post ? state.extra! as Post : null,
- ),
+ builder: (context, state) {
+ final extra = state.extra;
+ final Post? initialPost;
+ final List? 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',
diff --git a/lib/backend/models/app_update_info.dart b/lib/backend/models/app_update_info.dart
index bb8ef70..d042408 100644
--- a/lib/backend/models/app_update_info.dart
+++ b/lib/backend/models/app_update_info.dart
@@ -9,6 +9,7 @@ class AppUpdateInfo {
this.apkUrl,
this.windowsInstallerUrl,
this.portableZipUrl,
+ this.linuxTarGzUrl,
});
final String version;
@@ -20,7 +21,11 @@ class AppUpdateInfo {
final String? apkUrl;
final String? windowsInstallerUrl;
final String? portableZipUrl;
+ final String? linuxTarGzUrl;
bool get hasAssets =>
- apkUrl != null || windowsInstallerUrl != null || portableZipUrl != null;
+ apkUrl != null ||
+ windowsInstallerUrl != null ||
+ portableZipUrl != null ||
+ linuxTarGzUrl != null;
}
diff --git a/lib/backend/models/post.dart b/lib/backend/models/post.dart
index dc10c02..10e63dd 100644
--- a/lib/backend/models/post.dart
+++ b/lib/backend/models/post.dart
@@ -114,3 +114,14 @@ class Post {
const {},
);
}
+
+class PostNavigationContext {
+ const PostNavigationContext({
+ required this.currentPost,
+ required this.posts,
+ });
+
+ final Post currentPost;
+ final List posts;
+}
+
diff --git a/lib/backend/repositories/provider_repository.dart b/lib/backend/repositories/provider_repository.dart
index c8c943f..c50cb87 100644
--- a/lib/backend/repositories/provider_repository.dart
+++ b/lib/backend/repositories/provider_repository.dart
@@ -40,10 +40,10 @@ class ProviderRepository {
updatedAt: now,
),
ContentProviderConfig(
- id: 'safebooru',
- name: 'Safebooru',
- baseUrl: 'https://safebooru.org',
- apiType: 'gelbooru',
+ id: 'realbooru',
+ name: 'Realbooru',
+ baseUrl: 'https://realbooru.com',
+ apiType: 'realbooru_html',
enabled: true,
priority: 2,
timeoutSeconds: 20,
@@ -51,44 +51,16 @@ class ProviderRepository {
createdAt: now,
updatedAt: now,
),
- ContentProviderConfig(
- id: 'konachan',
- name: 'Konachan',
- baseUrl: 'https://konachan.com',
- apiType: 'moebooru',
- enabled: true,
- priority: 3,
- timeoutSeconds: 20,
- customHeaders: const {},
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'yandere',
- name: 'Yande.re',
- baseUrl: 'https://yande.re',
- apiType: 'moebooru',
- enabled: true,
- priority: 4,
- timeoutSeconds: 20,
- customHeaders: const {
- 'query.api_version': '2',
- 'query.include_tags': '1',
- 'query.filter': '1',
- },
- createdAt: now,
- updatedAt: now,
- ),
ContentProviderConfig(
id: 'e621',
name: 'e621',
baseUrl: 'https://e621.net',
apiType: 'e621',
enabled: true,
- priority: 5,
+ priority: 3,
timeoutSeconds: 20,
customHeaders: const {
- 'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
+ 'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser',
},
createdAt: now,
updatedAt: now,
@@ -99,76 +71,10 @@ class ProviderRepository {
baseUrl: 'https://e926.net',
apiType: 'e621',
enabled: true,
- priority: 6,
+ priority: 4,
timeoutSeconds: 20,
customHeaders: const {
- 'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
- },
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'realbooru',
- name: 'Realbooru',
- baseUrl: 'https://realbooru.com',
- apiType: 'realbooru_html',
- enabled: true,
- priority: 7,
- timeoutSeconds: 20,
- customHeaders: const {},
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'xbooru',
- name: 'Xbooru',
- baseUrl: 'https://xbooru.com',
- apiType: 'gelbooru',
- enabled: true,
- priority: 8,
- timeoutSeconds: 20,
- customHeaders: const {},
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'paheal',
- name: 'Rule34 Paheal',
- baseUrl: 'https://rule34.paheal.net',
- apiType: 'paheal',
- enabled: true,
- priority: 9,
- timeoutSeconds: 20,
- customHeaders: const {
- 'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
- },
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'kemono',
- name: 'Kemono',
- baseUrl: 'https://kemono.su',
- apiType: 'kemono',
- enabled: true,
- priority: 10,
- timeoutSeconds: 20,
- customHeaders: const {
- 'query.creator_api_base': 'https://kemono-api.mbaharip.com',
- },
- createdAt: now,
- updatedAt: now,
- ),
- ContentProviderConfig(
- id: 'coomer',
- name: 'Coomer',
- baseUrl: 'https://coomer.su',
- apiType: 'coomer',
- enabled: false,
- priority: 11,
- timeoutSeconds: 20,
- customHeaders: const {
- 'query.creator_api_base': 'https://kemono-api.mbaharip.com',
+ 'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser',
},
createdAt: now,
updatedAt: now,
@@ -179,7 +85,7 @@ class ProviderRepository {
baseUrl: 'https://pawchive.pw',
apiType: 'pawchive',
enabled: true,
- priority: 12,
+ priority: 5,
timeoutSeconds: 20,
customHeaders: const {},
createdAt: now,
@@ -192,11 +98,22 @@ class ProviderRepository {
return _databaseService.safeWrite((isar) async {
final existingProviders =
await isar.providerConfigEntitys.where().findAll();
+ const removedIds = {
+ 'safebooru',
+ 'konachan',
+ 'yandere',
+ 'xbooru',
+ 'paheal',
+ 'kemono',
+ 'coomer',
+ };
for (final provider in existingProviders) {
if (provider.providerId == 'cosbooru') {
provider.enabled = false;
provider.updatedAt = DateTime.now();
await isar.providerConfigEntitys.put(provider);
+ } else if (removedIds.contains(provider.providerId)) {
+ await isar.providerConfigEntitys.delete(provider.isarId);
}
}
final seeds = seedProviders();
diff --git a/lib/backend/services/settings_service.dart b/lib/backend/services/settings_service.dart
index c6e3ec4..625aa15 100644
--- a/lib/backend/services/settings_service.dart
+++ b/lib/backend/services/settings_service.dart
@@ -48,12 +48,14 @@ class AppSettings {
required this.lastFeedProviderIds,
required this.lastFeedTopPeriod,
required this.lastFeedScrollOffset,
+ this.favoriteArtists = const [],
this.skippedUpdateVersion,
this.lastUpdateCheckAt,
this.defaultRatingFilter,
this.lastFeedRating,
});
+ final List favoriteArtists;
final List enabledProviderIds;
final bool nsfwEnabled;
final int cacheTtlHours;
@@ -96,11 +98,12 @@ class AppSettings {
final String? lastFeedRating;
static const defaults = AppSettings(
- enabledProviderIds: ['gelbooru', 'rule34', 'safebooru'],
+ enabledProviderIds: ['gelbooru', 'rule34', 'realbooru'],
nsfwEnabled: true,
cacheTtlHours: 24,
cacheMaxItems: 2000,
- providerPriority: {'gelbooru': 0, 'rule34': 1, 'safebooru': 2},
+ providerPriority: {'gelbooru': 0, 'rule34': 1, 'realbooru': 2},
+ favoriteArtists: [],
themeMode: 'dark',
languageCode: 'ru',
appSeedColor: 0xFFE84D8A,
@@ -180,8 +183,10 @@ class AppSettings {
String? defaultRatingFilter,
String? lastFeedRating,
bool clearLastFeedRating = false,
+ List? favoriteArtists,
}) {
return AppSettings(
+ favoriteArtists: favoriteArtists ?? this.favoriteArtists,
enabledProviderIds: enabledProviderIds ?? this.enabledProviderIds,
nsfwEnabled: nsfwEnabled ?? this.nsfwEnabled,
cacheTtlHours: cacheTtlHours ?? this.cacheTtlHours,
@@ -233,6 +238,7 @@ class AppSettings {
}
Map toJson() => {
+ 'favoriteArtists': favoriteArtists,
'enabledProviderIds': enabledProviderIds,
'nsfwEnabled': nsfwEnabled,
'cacheTtlHours': cacheTtlHours,
@@ -365,6 +371,9 @@ class AppSettings {
lastFeedScrollOffset:
(json['lastFeedScrollOffset'] as num?)?.toDouble() ??
defaults.lastFeedScrollOffset,
+ favoriteArtists: List.from(
+ (json['favoriteArtists'] as List?) ?? defaults.favoriteArtists,
+ ),
skippedUpdateVersion: json['skippedUpdateVersion'] as String?,
lastUpdateCheckAt: json['lastUpdateCheckAt'] as String?,
defaultRatingFilter: json['defaultRatingFilter'] as String?,
diff --git a/lib/backend/services/update_service.dart b/lib/backend/services/update_service.dart
index 2eb1068..aa5e305 100644
--- a/lib/backend/services/update_service.dart
+++ b/lib/backend/services/update_service.dart
@@ -11,9 +11,9 @@ class UpdateService {
UpdateService(this._dio, this._settingsService);
static const latestReleaseUrl =
- 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/RuleGelApp/releases/latest';
+ 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases/latest';
static const releasesUrl =
- 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/RuleGelApp/releases';
+ 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases';
final Dio _dio;
final SettingsService _settingsService;
@@ -100,6 +100,7 @@ class UpdateService {
String? apkUrl;
String? installerUrl;
String? portableUrl;
+ String? linuxTarGzUrl;
for (final item in assets.whereType