Compare commits

...
8 Commits
Author SHA1 Message Date
RarDog 8ff2efd84c feat: полный ребрендинг интерфейса, User-Agent и системных служб в Prisma (v3.4.1)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 11:02:09 +03:00
RarDog 583f60e706 feat: ребрендинг в Prisma и улучшенная прокрутка анонсов автора (v3.4.0)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 10:55:10 +03:00
RarDog f983e3c8c0 feat: исключение дублирования в текстовых публикациях и вкладки зеркал (v3.3.1)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 10:40:14 +03:00
RarDog 67c6994311 feat: redesign artist messages/announcements & native text-only posts (v3.3.0)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 10:24:20 +03:00
RarDog a88a8b5050 feat: fix fullscreen video playback retention, orientation layout and clean up UI (v3.2.0)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-05 09:53:10 +03:00
RarDog a039f09ec3 feat: Fullscreen 2.0 with instant handoff, screen brightness gestures, and real-time scrub preview (v3.1.9)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:43:46 +03:00
RarDog c7f507836b fix: fullscreen playback state retention and responsive control buttons (v3.1.8)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:33:42 +03:00
RarDog 1395cfd414 fix: remove unsafe libmpv setProperty FFI calls causing SIGSEGV crash (v3.1.7)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:17:15 +03:00
48 changed files with 3231 additions and 2263 deletions
+11 -11
View File
@@ -1,7 +1,7 @@
# 🌙 Lunaris # 💎 Prisma
<p align="center"> <p align="center">
<img src="android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="128" height="128" alt="Lunaris Logo" style="border-radius: 28px; box-shadow: 0 10px 30px rgba(0,0,0,0.35);" /> <img src="android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" width="128" height="128" alt="Prisma Logo" style="border-radius: 28px; box-shadow: 0 10px 30px rgba(0,0,0,0.35);" />
</p> </p>
<p align="center"> <p align="center">
@@ -10,16 +10,16 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.1.6-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.1.6" /></a> <a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.4.1-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.4.1" /></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-00C853?style=for-the-badge" alt="Platforms" /> <img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-00C853?style=for-the-badge" alt="Platforms" />
<a href="https://github.com/RarDog/Lunaris/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>
<img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License MIT" /> <img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License MIT" />
</p> </p>
--- ---
## 🚀 Что нового в Lunaris 3.0 (Major Release) ## 🚀 Что нового в Prisma 3.0 (Major Release)
### 🎨 Дизайн 2.0 ### 🎨 Дизайн 2.0
- **Современный визуальный стиль**: Полный переход на squircle-карточки, адаптивную цветовую палитру и улучшенную Material You динамическую тему. - **Современный визуальный стиль**: Полный переход на squircle-карточки, адаптивную цветовую палитру и улучшенную Material You динамическую тему.
@@ -28,7 +28,7 @@
### 🐾 Глубокая интеграция Pawchive 2.0 ### 🐾 Глубокая интеграция Pawchive 2.0
- **Мульти-аккаунты**: Поддержка авторизации по логину и паролю, а также прямому вводу `Session Cookie`. - **Мульти-аккаунты**: Поддержка авторизации по логину и паролю, а также прямому вводу `Session Cookie`.
- **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Lunaris и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап. - **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Prisma и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап.
- **Моментальная синхронизация**: При нажатии кнопки «В избранное» у любого автора (Fanbox, Patreon, Fantia, Boosty, Gumroad, DLsite, Discord) статус синхронизируется с сервером на лету. - **Моментальная синхронизация**: При нажатии кнопки «В избранное» у любого автора (Fanbox, Patreon, Fantia, Boosty, Gumroad, DLsite, Discord) статус синхронизируется с сервером на лету.
### 🎬 Умный медиаплеер и определение разрешения ### 🎬 Умный медиаплеер и определение разрешения
@@ -43,7 +43,7 @@
## 📚 Документация и Вики (Руководство пользователя) ## 📚 Документация и Вики (Руководство пользователя)
Подробные иллюстрированные руководства доступны в **[Lunaris Wiki](https://github.com/RarDog/Lunaris/wiki)** и прямо в репозитории: Подробные иллюстрированные руководства доступны в **[Prisma Wiki](https://github.com/RarDog/Prisma/wiki)** и прямо в репозитории:
- 🚀 **[Быстрый старт и установка](docs/wiki/Getting-Started.md)** — выбор правильного APK под процессор, запуск на Linux. - 🚀 **[Быстрый старт и установка](docs/wiki/Getting-Started.md)** — выбор правильного APK под процессор, запуск на Linux.
- 🌐 **[Провайдеры контента](docs/wiki/Providers.md)** — особенности Booru (Gelbooru, Rule34, Realbooru, e621) и архивов Pawchive. - 🌐 **[Провайдеры контента](docs/wiki/Providers.md)** — особенности Booru (Gelbooru, Rule34, Realbooru, e621) и архивов Pawchive.
@@ -81,7 +81,7 @@
## 📥 Загрузка (Скачать сборки) ## 📥 Загрузка (Скачать сборки)
Релизы доступны на странице **[GitHub Releases](https://github.com/RarDog/Lunaris/releases)**. Релизы доступны на странице **[GitHub Releases](https://github.com/RarDog/Prisma/releases)**.
| Платформа | Архитектура | Формат | Описание | | Платформа | Архитектура | Формат | Описание |
|---|---|---|---| |---|---|---|---|
@@ -105,8 +105,8 @@
```bash ```bash
# Клонирование репозитория # Клонирование репозитория
git clone https://github.com/RarDog/Lunaris.git git clone https://github.com/RarDog/Prisma.git
cd Lunaris cd Prisma
# Установка зависимостей # Установка зависимостей
flutter pub get flutter pub get
@@ -155,7 +155,7 @@ lib/
## 🔒 Безопасность и конфиденциальность ## 🔒 Безопасность и конфиденциальность
- **100% локальное хранение**: Вся история, закладки, локальные авторы и конфигурации хранятся только в вашей локальной базе на устройстве. - **100% локальное хранение**: Вся история, закладки, локальные авторы и конфигурации хранятся только в вашей локальной базе на устройстве.
- **Никакой телеметрии**: Lunaris не собирает аналитику, не отправляет логи и уважает вашу приватность. - **Никакой телеметрии**: Prisma не собирает аналитику, не отправляет логи и уважает вашу приватность.
- **Прямое соединение**: Запросы отправляются напрямую к выбранным API без прокси-серверов посредников. - **Прямое соединение**: Запросы отправляются напрямую к выбранным API без прокси-серверов посредников.
--- ---
+1 -1
View File
@@ -3,7 +3,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application <application
android:label="Lunaris" android:label="Prisma"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
+4 -4
View File
@@ -1,7 +1,7 @@
# ❓ Часто задаваемые вопросы (FAQ) # ❓ Часто задаваемые вопросы (FAQ)
### 1. Как обновить приложение? ### 1. Как обновить приложение?
Lunaris проверяет наличие свежих версий автоматически при запуске. Prisma проверяет наличие свежих версий автоматически при запуске.
Вы также можете нажать **«Проверить обновления»** в Настройках. Вы также можете нажать **«Проверить обновления»** в Настройках.
Приложение определит архитектуру вашего Android-устройства и скачает оптимальный APK напрямую из репозитория. Приложение определит архитектуру вашего Android-устройства и скачает оптимальный APK напрямую из репозитория.
@@ -13,13 +13,13 @@ Lunaris проверяет наличие свежих версий автома
--- ---
### 3. Где сохраняются скачанные файлы? ### 3. Где сохраняются скачанные файлы?
- **На Android**: В стандартную папку `Pictures/Lunaris` или `Download/Lunaris` (в зависимости от настроек системы). Файлы сразу видны в стандартной Галерее телефона. - **На Android**: В стандартную папку `Pictures/Prisma` или `Download/Prisma` (в зависимости от настроек системы). Файлы сразу видны в стандартной Галерее телефона.
- **На Linux**: В директорию `~/Pictures/Lunaris` или папку загрузок. - **На Linux**: В директорию `~/Pictures/Prisma` или папку загрузок.
--- ---
### 4. Безопасно ли вводить логин и пароль от Pawchive? ### 4. Безопасно ли вводить логин и пароль от Pawchive?
Абсолютно безопасно. Lunaris не отправляет ваши учетные данные никуда, кроме официального защищенного API Pawchive по HTTPS. Пароли не логируются и сохраняются в зашифрованном системном хранилище. Для дополнительного спокойствия вы можете использовать вход через **Session Cookie**. Абсолютно безопасно. Prisma не отправляет ваши учетные данные никуда, кроме официального защищенного API Pawchive по HTTPS. Пароли не логируются и сохраняются в зашифрованном системном хранилище. Для дополнительного спокойствия вы можете использовать вход через **Session Cookie**.
--- ---
+8 -8
View File
@@ -2,9 +2,9 @@
### 📥 1. Установка на Android ### 📥 1. Установка на Android
Релизы приложения публикуются на странице **[GitHub Releases](https://github.com/RarDog/Lunaris/releases)**. Релизы приложения публикуются на странице **[GitHub Releases](https://github.com/RarDog/Prisma/releases)**.
Начиная с версии **Lunaris 3.0**, APK разделены по архитектуре процессора, чтобы уменьшить размер скачиваемого файла: Начиная с версии **Prisma 3.0**, APK разделены по архитектуре процессора, чтобы уменьшить размер скачиваемого файла:
| Файл APK | Для каких устройств | | Файл APK | Для каких устройств |
|---|---| |---|---|
@@ -13,27 +13,27 @@
| **`app-x86_64-release.apk`** | Эмуляторы Android на ПК (Bluestacks, Android Studio) или планшеты с процессорами Intel/AMD. | | **`app-x86_64-release.apk`** | Эмуляторы Android на ПК (Bluestacks, Android Studio) или планшеты с процессорами Intel/AMD. |
> [!TIP] > [!TIP]
> **Умное автообновление:** При проверке обновлений прямо внутри Lunaris приложение **самостоятельно определит архитектуру** вашего процессора и автоматически скачает правильный файл! > **Умное автообновление:** При проверке обновлений прямо внутри Prisma приложение **самостоятельно определит архитектуру** вашего процессора и автоматически скачает правильный файл!
#### Шаги установки: #### Шаги установки:
1. Скачайте нужный `.apk` файл на устройство. 1. Скачайте нужный `.apk` файл на устройство.
2. Откройте файл через менеджер файлов и подтвердите установку («Разрешить установку из этого источника»). 2. Откройте файл через менеджер файлов и подтвердите установку («Разрешить установку из этого источника»).
3. Запустите **Lunaris**. 3. Запустите **Prisma**.
--- ---
### 🐧 2. Установка на Linux ### 🐧 2. Установка на Linux
Lunaris распространяется для 64-битных Linux в виде готового автономного архива: Prisma распространяется для 64-битных Linux в виде готового автономного архива:
1. Скачайте **`Lunaris-v3.0.0-linux-x64.tar.gz`**. 1. Скачайте **`Prisma-v3.0.0-linux-x64.tar.gz`**.
2. Распакуйте архив в любую удобную папку: 2. Распакуйте архив в любую удобную папку:
```bash ```bash
tar -xzf Lunaris-v3.0.0-linux-x64.tar.gz -C ~/Lunaris tar -xzf Prisma-v3.0.0-linux-x64.tar.gz -C ~/Prisma
``` ```
3. Запустите приложение: 3. Запустите приложение:
```bash ```bash
cd ~/Lunaris cd ~/Prisma
./lunaris ./lunaris
``` ```
+2 -2
View File
@@ -1,6 +1,6 @@
# 🌙 Добро пожаловать в Lunaris Wiki! # 🌙 Добро пожаловать в Prisma Wiki!
**Lunaris** — это современный, быстрый и функциональный кроссплатформенный клиент для Booru-борд и архивов независимых авторов (Creator Archives). **Prisma** — это современный, быстрый и функциональный кроссплатформенный клиент для Booru-борд и архивов независимых авторов (Creator Archives).
Приложение разработано на Flutter для **Android**, **Linux** и **Windows** с акцентом на плавный интерфейс в стиле Pinterest, глубокую кастомизацию, приватность и удобную работу с медиа. Приложение разработано на Flutter для **Android**, **Linux** и **Windows** с акцентом на плавный интерфейс в стиле Pinterest, глубокую кастомизацию, приватность и удобную работу с медиа.
+2 -2
View File
@@ -1,6 +1,6 @@
# 🎬 Медиаплеер и просмотр # 🎬 Медиаплеер и просмотр
Экран просмотра в Lunaris оптимизирован для быстрого, плавного и естественного взаимодействия с контентом. Экран просмотра в Prisma оптимизирован для быстрого, плавного и естественного взаимодействия с контентом.
--- ---
@@ -17,7 +17,7 @@
## 📐 Определение разрешения медиа (1920×1080 и др.) ## 📐 Определение разрешения медиа (1920×1080 и др.)
В Lunaris 3.0 внедрено **интеллектуальное ленивое определение разрешения**: В Prisma 3.0 внедрено **интеллектуальное ленивое определение разрешения**:
- Для изображений и видео в инфо-панели поста отображаются точные физические размеры (например, `1920 × 1080`, `3840 × 2160`). - Для изображений и видео в инфо-панели поста отображаются точные физические размеры (например, `1920 × 1080`, `3840 × 2160`).
- Определение происходит динамически через заголовки и потоковый декодер — приложению не требуется скачивать многомегабайтный файл целиком только ради того, чтобы узнать его габариты. - Определение происходит динамически через заголовки и потоковый декодер — приложению не требуется скачивать многомегабайтный файл целиком только ради того, чтобы узнать его габариты.
+2 -2
View File
@@ -1,6 +1,6 @@
# 💾 Офлайн-режим и загрузки # 💾 Офлайн-режим и загрузки
Lunaris предоставляет полноценный оффлайн-хаб для работы с вашей личной коллекцией без подключения к интернету. Prisma предоставляет полноценный оффлайн-хаб для работы с вашей личной коллекцией без подключения к интернету.
--- ---
@@ -26,7 +26,7 @@ Lunaris предоставляет полноценный оффлайн-хаб
*Пример шаблона:* *Пример шаблона:*
```text ```text
Lunaris/{Provider}/{Artist}/{ID}.{ext} Prisma/{Provider}/{Artist}/{ID}.{ext}
``` ```
Файлы будут аккуратно раскладываться по папкам авторов и провайдеров, сохраняя порядок в вашей галерее. Файлы будут аккуратно раскладываться по папкам авторов и провайдеров, сохраняя порядок в вашей галерее.
+1 -1
View File
@@ -1,6 +1,6 @@
# 🐾 Интеграция и синхронизация с Pawchive # 🐾 Интеграция и синхронизация с Pawchive
Начиная с версии **Lunaris 3.0**, в приложении реализована глубокая интеграция с личным профилем Pawchive. Начиная с версии **Prisma 3.0**, в приложении реализована глубокая интеграция с личным профилем Pawchive.
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
# 🌐 Провайдеры контента # 🌐 Провайдеры контента
Lunaris объединяет два фундаментальных класса источников: классические **Booru-доски** и современные **архивы авторов (Pawchive)**. Prisma объединяет два фундаментальных класса источников: классические **Booru-доски** и современные **архивы авторов (Pawchive)**.
--- ---
@@ -17,7 +17,7 @@ Lunaris объединяет два фундаментальных класса
- `gumroad` (Gumroad) - `gumroad` (Gumroad)
- `dlsite` (DLsite) - `dlsite` (DLsite)
- `discord` (Серверы Discord) - `discord` (Серверы Discord)
- **Возможности в Lunaris:** - **Возможности в Prisma:**
- Просмотр глобальной ленты свежих постов. - Просмотр глобальной ленты свежих постов.
- Персональные карточки авторов с аватаром, баннером, анонсами и ссылками на оригинальные профили. - Персональные карточки авторов с аватаром, баннером, анонсами и ссылками на оригинальные профили.
- Полноценная авторизация в профиль Pawchive (логин/пароль или Session Cookie). - Полноценная авторизация в профиль Pawchive (логин/пароль или Session Cookie).
+1 -1
View File
@@ -1,6 +1,6 @@
# 🛡️ Черный список и фильтрация # 🛡️ Черный список и фильтрация
Lunaris оснащен многоуровневой системой защиты от нежелательного контента, работающей прямо на устройстве. Prisma оснащен многоуровневой системой защиты от нежелательного контента, работающей прямо на устройстве.
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
# 🔍 Умный поиск и теги # 🔍 Умный поиск и теги
Поиск в Lunaris спроектирован для мгновенного отклика и максимальной гибкости в построении сложных запросов. Поиск в Prisma спроектирован для мгновенного отклика и максимальной гибкости в построении сложных запросов.
--- ---
@@ -21,7 +21,7 @@
Классические Booru-серверы при поиске нескольких тегов (`tag1 tag2`) возвращают только посты, где **присутствуют оба тега одновременно**. Классические Booru-серверы при поиске нескольких тегов (`tag1 tag2`) возвращают только посты, где **присутствуют оба тега одновременно**.
Lunaris поддерживает специальный оператор **`and`**: Prisma поддерживает специальный оператор **`and`**:
```text ```text
genshin_impact and honkai_star_rail genshin_impact and honkai_star_rail
``` ```
+3 -3
View File
@@ -1,4 +1,4 @@
### 🌙 Lunaris Wiki ### 🌙 Prisma Wiki
- **[Главная](Home)** - **[Главная](Home)**
- **[Быстрый старт и установка](Getting-Started)** - **[Быстрый старт и установка](Getting-Started)**
@@ -12,5 +12,5 @@
--- ---
**[Скачать Lunaris](https://github.com/RarDog/Lunaris/releases)** **[Скачать Prisma](https://github.com/RarDog/Prisma/releases)**
**[Репозиторий GitHub](https://github.com/RarDog/Lunaris)** **[Репозиторий GitHub](https://github.com/RarDog/Prisma)**
@@ -1,4 +1,4 @@
#define MyAppName "Lunaris" #define MyAppName "Prisma"
#define MyAppVersion "2.0.1" #define MyAppVersion "2.0.1"
#define MyAppPublisher "RarDog" #define MyAppPublisher "RarDog"
#define MyAppExeName "gel_rule_app.exe" #define MyAppExeName "gel_rule_app.exe"
@@ -13,7 +13,7 @@ DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
PrivilegesRequired=lowest PrivilegesRequired=lowest
OutputDir=..\dist OutputDir=..\dist
OutputBaseFilename=LunarisSetup OutputBaseFilename=PrismaSetup
SetupIconFile=..\windows\runner\resources\app_icon.ico SetupIconFile=..\windows\runner\resources\app_icon.ico
Compression=lzma2 Compression=lzma2
SolidCompression=yes SolidCompression=yes
+2 -2
View File
@@ -40,7 +40,7 @@ class GelRuleApp extends ConsumerWidget {
return DynamicColorBuilder( return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) { builder: (lightDynamic, darkDynamic) {
return MaterialApp.router( return MaterialApp.router(
title: 'Lunaris', title: 'Prisma',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: buildTheme( theme: buildTheme(
Brightness.light, Brightness.light,
@@ -97,7 +97,7 @@ class _AppOverlayState extends ConsumerState<_AppOverlay> {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('${strings.appUpdateAvailable}: Lunaris ${info.version}'), title: Text('${strings.appUpdateAvailable}: Prisma ${info.version}'),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),
child: SingleChildScrollView( child: SingleChildScrollView(
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.1.6'; const appDisplayVersion = '3.4.1';
const appBuildNumber = 67; const appBuildNumber = 75;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+132 -52
View File
@@ -1,5 +1,82 @@
const lunarisChangelog = [ const prismaChangelog = [
LunarisChange( PrismaChange(
version: '3.4.1',
title: 'Полный ребрендинг интерфейса: заголовок ленты, окно «О приложении» и системные службы',
bullets: [
'Полное обновление названия в интерфейсе: заголовок в шапке ленты заменен на Prisma, обновлен логотип и карточка в диалоге «О приложении».',
'Окна обновлений и чейнджлогов: все диалоги проверки обновлений, чейнджлога и бета-версий теперь используют имя Prisma.',
'Системные службы и User-Agent: сетевой стек, экспорт резервных копий и кэш тегов переведены на Prisma с автоматической миграцией сохраненных данных.',
],
),
PrismaChange(
version: '3.4.0',
title: 'Ребрендинг в Prisma: новое имя, комфортная прокрутка анонсов и обновленные репозитории',
bullets: [
'Ребрендинг в Prisma: приложение официально переименовано в Prisma — обновлены названия окон, манифесты и системные идентификаторы.',
'Улучшенная прокрутка анонсов: в диалоге личных сообщений и рассылок автора добавлен дополнительный нижний отступ, чтобы карточки не перекрывались нижней панелью навигации и системными элементами управления.',
'Синхронизация репозиториев: репозитории на GitHub и Gitea переведены на Prisma, ссылки внутри приложения и служба автообновления переключены на новые адреса.',
],
),
PrismaChange(
version: '3.3.1',
title: 'Улучшенное разделение текстовых постов и облачных зеркал',
bullets: [
'Исключение дублирования в текстовых публикациях: для текстовых постов (без медиа) убрана лишняя плашка «Облачные диски и зеркала», исключая повторный вывод того же текста внизу экрана.',
'Вкладки «Зеркала» и «Описание»: для постов с фото, видео и GIF в блоке облачных дисков теперь доступны удобные переключаемые вкладки, позволяющие отдельно просматривать список ссылок на хранилища и авторское описание.',
'Чистый и нативный вид карточек: если у медиапоста есть только ссылки или только описание, отображается аккуратная монолитная секция с соответствующим заголовком и иконкой без лишних переключателей.',
],
),
PrismaChange(
version: '3.3.0',
title: 'Редизайн личных сообщений и рассылок артиста, нативная поддержка текстовых публикаций',
bullets: [
'Центр сообщений и рассылок автора: полностью переработан диалог по кнопке-рупору на экране артиста — аккуратные карточки сообщений в стиле мессенджера, бейджи сервисов, точное время публикации и кнопка быстрого копирования текста в буфер обмена.',
'Нативная поддержка текстовых постов: публикации без медиафайлов больше не вызывают ошибок и пустых плейсхолдеров — в сетке они отображаются как стильные заметки с заголовком и выдержкой текста.',
'Удобное чтение статей и заметок: при открытии текстового поста отображается комфортный ридер с форматированным текстом, ссылками автора и тегами.',
'Фильтрация по типу контента: на экране автора добавлен фильтр «Текст» с подсчетом количества публикаций наряду с фото, видео и GIF.',
'Поддержка анонсов в Kemono и Coomer: API анонсов и персональных сообщений теперь полноценно работает для всех поддерживаемых провайдеров.',
],
),
PrismaChange(
version: '3.2.0',
title: 'Стабильный полноэкранный режим: сохранение воспроизведения, устранение вылетов плеера и чистый интерфейс',
bullets: [
'Сохранение воспроизведения видео при повороте экрана: исправлена логика адаптивности на смартфонах — альбомная ориентация больше не сбрасывает мобильный интерфейс и не уничтожает запущенный плеер.',
'Защита жизненного цикла Player в фоне: плеер не уничтожается при переходе в полный экран, а кнопки «Пуск/Пауза» и перемотки работают мгновенно и безотказно.',
'Чистый кинематографичный интерфейс: удалены старые дублирующиеся кнопки в правом верхнем углу; кнопка смены ориентации (альбомный/портретный режим) аккуратно интегрирована в верхнюю панель управления.',
'Быстрое скрытие и показ контролов: касание свободной области видео плавно переключает видимость панели управления, а при нажатии на кнопки таймер автоскрытия автоматически обновляется.',
],
),
PrismaChange(
version: '3.1.9',
title: 'Полноэкранный плеер 2.0: мгновенный переход без задержек, жесты яркости и скраббинг с превью',
bullets: [
'Мгновенный переход в полноэкранный режим: убраны промежуточные задержки и наложения текстур, вход и выход происходят без малейших задержек.',
'Двухзонное жестовое управление: левая сторона экрана регулирует яркость подсветки с индикатором солнца, правая сторона — громкость звука.',
'Плавный скраббинг с плавающим таймингом: перемещение по таймлайну видео теперь отображает интерактивную плашку точного времени, а контролы не исчезают во время удержания ползунка.',
'Сброс яркости: при выходе из полноэкранного режима системный уровень яркости экрана аккуратно восстанавливается.',
],
),
PrismaChange(
version: '3.1.8',
title: 'Безупречный полноэкранный плеер: устранение остановки при развертывании и отзывчивые кнопки управления',
bullets: [
'Непрерывное воспроизведение при открытии на весь экран: видео больше не останавливается самопроизвольно при входе в полноэкранный режим или смене ориентации устройства.',
'Корректный статус воспроизведения: устранен сброс кнопки на «Пуск» при переходе в полный экран — видео продолжает воспроизведение без ложных остановок.',
'Мгновенный отклик кнопок «Пуск/Пауза» и перемотки: жесты управления поверхностью (двойной тап, смахивание громкости) перемещены в фоновый слой, исключая перехват и отмену нажатий по кнопкам управления.',
'Синхронизация состояния при сворачивании: плеер точно запоминает состояние паузы/воспроизведения при выходе из полноэкранного режима обратно в пост.',
],
),
PrismaChange(
version: '3.1.7',
title: 'Устранение критического сбоя при открытии видеопоста (SIGSEGV в libmpv)',
bullets: [
'Устранение нативного вылета: полностью удалены небезопасные прямые FFI-вызовы setProperty к mpv, приводившие к немедленному падению приложения на Android при инициализации видео.',
'Безопасная конфигурация декодирования: перевод настроек аппаратного ускорения и фоллбэка кодеков на официальный типобезопасный VideoControllerConfiguration (media_kit_video).',
'Надежный фоллбэк при редких ошибках кодека: при несовместимости аппаратного декодера плеер бесшовно переключается на программный FFmpeg-декодер без риска краша.',
],
),
PrismaChange(
version: '3.1.6', version: '3.1.6',
title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека', title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека',
bullets: [ bullets: [
@@ -9,7 +86,7 @@ const lunarisChangelog = [
'Автоматический fallback для ошибки Could not open codec: видеоплеер получил безопасные профили декодирования и автопереключение на программный лавк-декодер (FFmpeg software fallback) при редких сбоях аппаратного ускорения.', 'Автоматический fallback для ошибки Could not open codec: видеоплеер получил безопасные профили декодирования и автопереключение на программный лавк-декодер (FFmpeg software fallback) при редких сбоях аппаратного ускорения.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.5', version: '3.1.5',
title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев', title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев',
bullets: [ bullets: [
@@ -20,7 +97,7 @@ const lunarisChangelog = [
'Удобный доступ к комментариям в посте: увеличен отступ снизу в деталях поста и расширена зона клика блока комментариев, позволяя комфортно прокрутить их выше системных жестов навигации.', 'Удобный доступ к комментариям в посте: увеличен отступ снизу в деталях поста и расширена зона клика блока комментариев, позволяя комфортно прокрутить их выше системных жестов навигации.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.4', version: '3.1.4',
title: 'Оптимизация видеоплеера — адаптивная высота и идеальное прилегание полосы воспроизведения', title: 'Оптимизация видеоплеера — адаптивная высота и идеальное прилегание полосы воспроизведения',
bullets: [ bullets: [
@@ -29,7 +106,7 @@ const lunarisChangelog = [
'Компактные элементы управления: выровнены буферный трек и слайдер прокрутки, оптимизирован градиент затенения для максимальной видимости и удобства управления.', 'Компактные элементы управления: выровнены буферный трек и слайдер прокрутки, оптимизирован градиент затенения для максимальной видимости и удобства управления.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.3', version: '3.1.3',
title: 'iPhone Liquid Glass — Источники, Проверка сети и Навигация назад по вкладкам', title: 'iPhone Liquid Glass — Источники, Проверка сети и Навигация назад по вкладкам',
bullets: [ bullets: [
@@ -39,7 +116,7 @@ const lunarisChangelog = [
'Liquid Glass редизайн проверки состояния источников (Provider Check): карточка общих метрик сети (онлайн/офлайн, средний пинг), раскрывающиеся аккордеоны детальной диагностики, живые светодиодные LED-индикаторы статуса и кнопка одновременной проверки всех провайдеров.', 'Liquid Glass редизайн проверки состояния источников (Provider Check): карточка общих метрик сети (онлайн/офлайн, средний пинг), раскрывающиеся аккордеоны детальной диагностики, живые светодиодные LED-индикаторы статуса и кнопка одновременной проверки всех провайдеров.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.2', version: '3.1.2',
title: 'Оптимизация производительности: устранение лагов при скролле к тегам', title: 'Оптимизация производительности: устранение лагов при скролле к тегам',
bullets: [ bullets: [
@@ -49,7 +126,7 @@ const lunarisChangelog = [
'Интеллектуальный лимит начального отображения: оптимизировано первоначальное количество отображаемых тегов в огромных категориях с удобной кнопкой быстрого раскрытия.', 'Интеллектуальный лимит начального отображения: оптимизировано первоначальное количество отображаемых тегов в огромных категориях с удобной кнопкой быстрого раскрытия.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.1', version: '3.1.1',
title: 'Деление тегов по стилю e621/Gelbooru/Rule34 и исправление поиска тегов со скобками', title: 'Деление тегов по стилю e621/Gelbooru/Rule34 и исправление поиска тегов со скобками',
bullets: [ bullets: [
@@ -61,7 +138,7 @@ const lunarisChangelog = [
'Liquid Glass оформление блока тегов в детальном просмотре поста на мобильных и десктоп устройствах.', 'Liquid Glass оформление блока тегов в детальном просмотре поста на мобильных и десктоп устройствах.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.1.0', version: '3.1.0',
title: 'iPhone Liquid Glass — Поиск, Избранное, История и исправление доступности', title: 'iPhone Liquid Glass — Поиск, Избранное, История и исправление доступности',
bullets: [ bullets: [
@@ -71,7 +148,7 @@ const lunarisChangelog = [
'Тактильный отклик и стеклянные пилюли: быстрые операторы, категории, недавние запросы, фильтры медиа и переключатели разделов получили матовое оформление и вибрационный отклик Haptic Feedback.', 'Тактильный отклик и стеклянные пилюли: быстрые операторы, категории, недавние запросы, фильтры медиа и переключатели разделов получили матовое оформление и вибрационный отклик Haptic Feedback.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.9', version: '3.0.9',
title: 'Коррекция позиции кнопки настроек и отображения кнопок JSON и бэкапа', title: 'Коррекция позиции кнопки настроек и отображения кнопок JSON и бэкапа',
bullets: [ bullets: [
@@ -80,7 +157,7 @@ const lunarisChangelog = [
'Улучшенный дизайн и сетка кнопок экспорта/импорта: кнопки бэкапа и JSON организованы в симметричные аккуратные строки с яркими акцентными бейджами.', 'Улучшенный дизайн и сетка кнопок экспорта/импорта: кнопки бэкапа и JSON организованы в симметричные аккуратные строки с яркими акцентными бейджами.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.8', version: '3.0.8',
title: 'iPhone Liquid Glass — раздел «Настройки» и парящая стеклянная кнопка', title: 'iPhone Liquid Glass — раздел «Настройки» и парящая стеклянная кнопка',
bullets: [ bullets: [
@@ -90,7 +167,7 @@ const lunarisChangelog = [
'Стеклянная быстрая навигация по категориям: зафиксированная панель категорий с размытием фона и тактильным откликом при переключении разделов.', 'Стеклянная быстрая навигация по категориям: зафиксированная панель категорий с размытием фона и тактильным откликом при переключении разделов.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.7', version: '3.0.7',
title: 'Liquid Glass Navigation Bar — парящая стеклянная панель и анимированная пилюля', title: 'Liquid Glass Navigation Bar — парящая стеклянная панель и анимированная пилюля',
bullets: [ bullets: [
@@ -100,7 +177,7 @@ const lunarisChangelog = [
'Глубина контента: контент ленты, поиска и галереи плавно просвечивает и размывается под стеклянной панелью при прокрутке.', 'Глубина контента: контент ленты, поиска и галереи плавно просвечивает и размывается под стеклянной панелью при прокрутке.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.6', version: '3.0.6',
title: 'Улучшение недавних запросов и отображения тегов', title: 'Улучшение недавних запросов и отображения тегов',
bullets: [ bullets: [
@@ -110,7 +187,7 @@ const lunarisChangelog = [
'Удобное удаление: увеличенная область нажатия для кнопки удаления (крестика) предотвращает случайные нажатия на сам запрос.', 'Удобное удаление: увеличенная область нажатия для кнопки удаления (крестика) предотвращает случайные нажатия на сам запрос.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.5', version: '3.0.5',
title: 'Интерактивные ссылки на соцсети и авторов в объявлениях и описаниях', title: 'Интерактивные ссылки на соцсети и авторов в объявлениях и описаниях',
bullets: [ bullets: [
@@ -119,7 +196,7 @@ const lunarisChangelog = [
'Корректная очистка пунктуации: знаки препинания в конце URL (!, ?, :, ., ,, ;, >) больше не ломают ссылки.', 'Корректная очистка пунктуации: знаки препинания в конце URL (!, ?, :, ., ,, ;, >) больше не ломают ссылки.',
], ],
), ),
LunarisChange( PrismaChange(
version: '3.0.0', version: '3.0.0',
title: 'Lunaris 3.0 — Глобальное обновление: Дизайн 2.0, Интеграция Pawchive и умная синхронизация', title: 'Lunaris 3.0 — Глобальное обновление: Дизайн 2.0, Интеграция Pawchive и умная синхронизация',
bullets: [ bullets: [
@@ -132,7 +209,7 @@ const lunarisChangelog = [
'Исправление вёрстки: устранены переносы текста в заголовке окна аккаунтов и карточках пользователей на экранах смартфонов.', 'Исправление вёрстки: устранены переносы текста в заголовке окна аккаунтов и карточках пользователей на экранах смартфонов.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.10', version: '2.3.10',
title: 'Выгрузка локального избранного в Pawchive и двусторонняя синхронизация', title: 'Выгрузка локального избранного в Pawchive и двусторонняя синхронизация',
bullets: [ bullets: [
@@ -142,7 +219,7 @@ const lunarisChangelog = [
'Синхронизация в реальном времени с любых провайдеров: при добавлении автора в избранное в лентах Kemono, Coomer или Pawchive изменение мгновенно передаётся на сервер Pawchive.', 'Синхронизация в реальном времени с любых провайдеров: при добавлении автора в избранное в лентах Kemono, Coomer или Pawchive изменение мгновенно передаётся на сервер Pawchive.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.9', version: '2.3.9',
title: 'Исправление входа и синхронизации аккаунтов Pawchive', title: 'Исправление входа и синхронизации аккаунтов Pawchive',
bullets: [ bullets: [
@@ -151,7 +228,7 @@ const lunarisChangelog = [
'Информативные ошибки: понятные сообщения на русском при неверном пароле или сбое сети вместо сырых исключений Dio.', 'Информативные ошибки: понятные сообщения на русском при неверном пароле или сбое сети вместо сырых исключений Dio.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.8', version: '2.3.8',
title: 'ABI-Aware Auto-Update — правильный APK для вашего процессора', title: 'ABI-Aware Auto-Update — правильный APK для вашего процессора',
bullets: [ bullets: [
@@ -161,7 +238,7 @@ const lunarisChangelog = [
'Кеш ABI: архитектура определяется один раз за сеанс.', 'Кеш ABI: архитектура определяется один раз за сеанс.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.7', version: '2.3.7',
title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers', title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers',
bullets: [ bullets: [
@@ -170,7 +247,7 @@ const lunarisChangelog = [
'Session Cache: resolved image dimensions are cached in memory for the lifetime of the app so repeated visits to the same post do not trigger re-downloads.', 'Session Cache: resolved image dimensions are cached in memory for the lifetime of the app so repeated visits to the same post do not trigger re-downloads.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.6', version: '2.3.6',
title: 'Pawchive Accounts Integration & Favorites Synchronization', title: 'Pawchive Accounts Integration & Favorites Synchronization',
bullets: [ bullets: [
@@ -181,7 +258,7 @@ const lunarisChangelog = [
'Dedicated UI & Quick Access: manage connected accounts, view sync stats, and trigger synchronization from the Artists screen, Favorites modal, and Settings screen.', 'Dedicated UI & Quick Access: manage connected accounts, view sync stats, and trigger synchronization from the Artists screen, Favorites modal, and Settings screen.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.5', version: '2.3.5',
title: 'Fullscreen Video Auto-Rotation & Orientation Controls', title: 'Fullscreen Video Auto-Rotation & Orientation Controls',
bullets: [ bullets: [
@@ -190,7 +267,7 @@ const lunarisChangelog = [
'Clean Orientation Recovery: exiting fullscreen seamlessly restores system navigation bars (Edge-to-Edge) and restores unrestricted device sensor orientations.', 'Clean Orientation Recovery: exiting fullscreen seamlessly restores system navigation bars (Edge-to-Edge) and restores unrestricted device sensor orientations.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.4', version: '2.3.4',
title: 'OverlayPortal Architecture & Search UI Unblocking', title: 'OverlayPortal Architecture & Search UI Unblocking',
bullets: [ bullets: [
@@ -201,7 +278,7 @@ const lunarisChangelog = [
'Non-Blocking Outside Tap: tapping outside the suggestion dropdown smoothly dismisses suggestions while allowing the touch event to pass through directly to the underlying button or post.', 'Non-Blocking Outside Tap: tapping outside the suggestion dropdown smoothly dismisses suggestions while allowing the touch event to pass through directly to the underlying button or post.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.3', version: '2.3.3',
title: 'Tag Suggestions Overlay Fix & Rule34 API Alignment', title: 'Tag Suggestions Overlay Fix & Rule34 API Alignment',
bullets: [ bullets: [
@@ -211,7 +288,7 @@ const lunarisChangelog = [
'Rule34 Authentication & Outage Handling: added detection for "missing authentication", rate limits, and server overload/search down ("success": false) with informative guidance to enter API Key & User ID.', 'Rule34 Authentication & Outage Handling: added detection for "missing authentication", rate limits, and server overload/search down ("success": false) with informative guidance to enter API Key & User ID.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.2', version: '2.3.2',
title: 'Gelbooru DAPI Compliance & Tag Query Optimization', title: 'Gelbooru DAPI Compliance & Tag Query Optimization',
bullets: [ bullets: [
@@ -221,7 +298,7 @@ const lunarisChangelog = [
'Throttling & Auth Guidance: enhanced API credentials helper text and informative error messages with direct instructions to configure API Key & User ID from Gelbooru account options.', 'Throttling & Auth Guidance: enhanced API credentials helper text and informative error messages with direct instructions to configure API Key & User ID from Gelbooru account options.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.1', version: '2.3.1',
title: 'Cloud Mirrors & Author Announcements Fixes', title: 'Cloud Mirrors & Author Announcements Fixes',
bullets: [ bullets: [
@@ -230,7 +307,7 @@ const lunarisChangelog = [
'Author Announcements HTML Cleaning: stripped all raw <p style="">, <strong>, and formatting artifacts from artist announcements, rendering clean readable text and clickable service link chips.', 'Author Announcements HTML Cleaning: stripped all raw <p style="">, <strong>, and formatting artifacts from artist announcements, rendering clean readable text and clickable service link chips.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.3.0', version: '2.3.0',
title: 'Artist Cloud Drives & External Video Mirrors', title: 'Artist Cloud Drives & External Video Mirrors',
bullets: [ bullets: [
@@ -242,7 +319,7 @@ const lunarisChangelog = [
'Artist Commentary Card: formatted display of author notes, instructions, and text below post details.', 'Artist Commentary Card: formatted display of author notes, instructions, and text below post details.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.2.0', version: '2.2.0',
title: 'Offline Mode Overhaul & Post Details Screen Redesign', title: 'Offline Mode Overhaul & Post Details Screen Redesign',
bullets: [ bullets: [
@@ -253,7 +330,7 @@ const lunarisChangelog = [
'Unified Neighbor Strip & Tags Card: post neighbor thumbnail strip now available on both mobile and desktop, alongside modern rounded tag blocks and chat-bubble comments.', 'Unified Neighbor Strip & Tags Card: post neighbor thumbnail strip now available on both mobile and desktop, alongside modern rounded tag blocks and chat-bubble comments.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.1.0', version: '2.1.0',
title: '4-Tab Major Redesign: Search, Favorites, History & Collections', title: '4-Tab Major Redesign: Search, Favorites, History & Collections',
bullets: [ bullets: [
@@ -263,7 +340,7 @@ const lunarisChangelog = [
'Collections Tab Redesign: Pinterest-style collection cards with 2x2 thumbnail mosaic covers and glassmorphism post counters, modernized collection details screen with description banner, and elegant squircle creation dialog.', 'Collections Tab Redesign: Pinterest-style collection cards with 2x2 thumbnail mosaic covers and glassmorphism post counters, modernized collection details screen with description banner, and elegant squircle creation dialog.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.15', version: '2.0.15',
title: 'Settings 2.0 Layout Fixes & Pinned Quick Navigation', title: 'Settings 2.0 Layout Fixes & Pinned Quick Navigation',
bullets: [ bullets: [
@@ -274,7 +351,7 @@ const lunarisChangelog = [
'Header text overflow protections: wrapped title texts in Expanded across all settings cards.', 'Header text overflow protections: wrapped title texts in Expanded across all settings cards.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.14', version: '2.0.14',
title: 'Settings 2.0: Modern Grouped Design & Quick Ergonomics', title: 'Settings 2.0: Modern Grouped Design & Quick Ergonomics',
bullets: [ bullets: [
@@ -286,7 +363,7 @@ const lunarisChangelog = [
'Hero Brand Banner: polished app presentation card in About section with glowing icon, version build badge, update checker, and GitHub repository link.', 'Hero Brand Banner: polished app presentation card in About section with glowing icon, version build badge, update checker, and GitHub repository link.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.13', version: '2.0.13',
title: 'Toolbar Section Dividers & Post Card Badge Stacking Fix', title: 'Toolbar Section Dividers & Post Card Badge Stacking Fix',
bullets: [ bullets: [
@@ -296,7 +373,7 @@ const lunarisChangelog = [
'Expanded Bottom Gradient: increased bottom cinematic gradient height to 64dp for clean text and badge contrast.', 'Expanded Bottom Gradient: increased bottom cinematic gradient height to 64dp for clean text and badge contrast.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.12', version: '2.0.12',
title: 'Feed 2.0: Pinterest Modern Redesign & Sleek Cards', title: 'Feed 2.0: Pinterest Modern Redesign & Sleek Cards',
bullets: [ bullets: [
@@ -309,7 +386,7 @@ const lunarisChangelog = [
'Polished Search Presets: stylish bookmark capsules for quick access to saved search queries.', 'Polished Search Presets: stylish bookmark capsules for quick access to saved search queries.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.11', version: '2.0.11',
title: 'Mobile Video Player Redesign & Format Recognition Fix', title: 'Mobile Video Player Redesign & Format Recognition Fix',
bullets: [ bullets: [
@@ -323,7 +400,7 @@ const lunarisChangelog = [
'Sleek Neon Seekbar with live network buffer progress indicator.', 'Sleek Neon Seekbar with live network buffer progress indicator.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.10', version: '2.0.10',
title: 'Fix "and" Tag Operator Retention, Suggestion Preceding Drafts & AND Chip Styling', title: 'Fix "and" Tag Operator Retention, Suggestion Preceding Drafts & AND Chip Styling',
bullets: [ bullets: [
@@ -333,7 +410,7 @@ const lunarisChangelog = [
'Distinct visual styling for AND separator chips with secondary theme container and alt-route icon.', 'Distinct visual styling for AND separator chips with secondary theme container and alt-route icon.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.9', version: '2.0.9',
title: 'Instant Tag Suggestions, Persistent Disk Cache, Expanded History & Match Highlighting', title: 'Instant Tag Suggestions, Persistent Disk Cache, Expanded History & Match Highlighting',
bullets: [ bullets: [
@@ -347,7 +424,7 @@ const lunarisChangelog = [
'Dedicated Clear Tag Cache actions in Settings and Cache Manager.', 'Dedicated Clear Tag Cache actions in Settings and Cache Manager.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.8', version: '2.0.8',
title: 'Shared Context Modifiers for Multi-Tag Search & Clean Tag Filtering', title: 'Shared Context Modifiers for Multi-Tag Search & Clean Tag Filtering',
bullets: [ bullets: [
@@ -356,7 +433,7 @@ const lunarisChangelog = [
'Resolved "No posts yet" bug by adapting client post validation to multi-tag stream groups and stripping operator keywords from booru queries.', 'Resolved "No posts yet" bug by adapting client post validation to multi-tag stream groups and stripping operator keywords from booru queries.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.7', version: '2.0.7',
title: 'Independent Multi-Tag Search Streams, Local "and" Connector, Pawchive in Main Feed & Reverts', title: 'Independent Multi-Tag Search Streams, Local "and" Connector, Pawchive in Main Feed & Reverts',
bullets: [ bullets: [
@@ -367,7 +444,7 @@ const lunarisChangelog = [
'Removed external linked accounts bar from artist posts screen.', 'Removed external linked accounts bar from artist posts screen.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.6', version: '2.0.6',
title: 'Pawchive Deep Integration: Global Posts Feed, Artist Tags, Announcements, Links & Server Media Filter', title: 'Pawchive Deep Integration: Global Posts Feed, Artist Tags, Announcements, Links & Server Media Filter',
bullets: [ bullets: [
@@ -378,7 +455,7 @@ const lunarisChangelog = [
'Server-side media filtering (Photos, Videos, GIFs) querying the entire author archive through the API.', 'Server-side media filtering (Photos, Videos, GIFs) querying the entire author archive through the API.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.5', version: '2.0.5',
title: '10 QoL Super-Update: AMOLED, Dynamic Colors, Grid switch, Share, Presets, Cache & Backup', title: '10 QoL Super-Update: AMOLED, Dynamic Colors, Grid switch, Share, Presets, Cache & Backup',
bullets: [ bullets: [
@@ -393,7 +470,7 @@ const lunarisChangelog = [
'Full JSON backup export and import for seamless configuration sync.', 'Full JSON backup export and import for seamless configuration sync.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.4', version: '2.0.4',
title: 'Platform service selection & media type filtering in artist posts', title: 'Platform service selection & media type filtering in artist posts',
bullets: [ bullets: [
@@ -402,7 +479,7 @@ const lunarisChangelog = [
'Full-screen post pager respects active media type filters during swiping.', 'Full-screen post pager respects active media type filters during swiping.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.3', version: '2.0.3',
title: 'Infinite artist pagination, favorite artists modal, and card layout polish', title: 'Infinite artist pagination, favorite artists modal, and card layout polish',
bullets: [ bullets: [
@@ -412,7 +489,7 @@ const lunarisChangelog = [
'Removed mapping truncation in Pawchive provider to deliver all post attachments cleanly.', 'Removed mapping truncation in Pawchive provider to deliver all post attachments cleanly.',
], ],
), ),
LunarisChange( PrismaChange(
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',
bullets: [ bullets: [
@@ -424,7 +501,7 @@ const lunarisChangelog = [
'In-app update checks and downloads connected directly to RarDog GitHub releases.', 'In-app update checks and downloads connected directly to RarDog GitHub releases.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.1', version: '2.0.1',
title: 'Localization, zoom and favorite downloads', title: 'Localization, zoom and favorite downloads',
bullets: [ bullets: [
@@ -435,7 +512,7 @@ const lunarisChangelog = [
'Favorites can optionally auto-download media in the background, with local files tracked separately from favorites.', 'Favorites can optionally auto-download media in the background, with local files tracked separately from favorites.',
], ],
), ),
LunarisChange( PrismaChange(
version: '2.0.0', version: '2.0.0',
title: 'Customization and provider polish', title: 'Customization and provider polish',
bullets: [ bullets: [
@@ -446,7 +523,7 @@ const lunarisChangelog = [
'Kemono and Coomer creator search uses the current mbahArip keyword endpoints with public API fallbacks.', 'Kemono and Coomer creator search uses the current mbahArip keyword endpoints with public API fallbacks.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.1.2', version: '1.1.2',
title: 'Experimental tag and Artists update', title: 'Experimental tag and Artists update',
bullets: [ bullets: [
@@ -456,7 +533,7 @@ const lunarisChangelog = [
'This is an experimental release for checking the new tag and Artists behavior.', 'This is an experimental release for checking the new tag and Artists behavior.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.1.1', version: '1.1.1',
title: 'Artists and in-app updates', title: 'Artists and in-app updates',
bullets: [ bullets: [
@@ -466,7 +543,7 @@ const lunarisChangelog = [
'Tag input keeps its scroll position steadier while typing and committing chips.', 'Tag input keeps its scroll position steadier while typing and committing chips.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.1.0', version: '1.1.0',
title: 'Lunaris rebrand and providers', title: 'Lunaris rebrand and providers',
bullets: [ bullets: [
@@ -476,7 +553,7 @@ const lunarisChangelog = [
'Video player UI preferences are saved across fullscreen, details and restarts.', 'Video player UI preferences are saved across fullscreen, details and restarts.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.0.2', version: '1.0.2',
title: 'Provider safety fix', title: 'Provider safety fix',
bullets: [ bullets: [
@@ -485,7 +562,7 @@ const lunarisChangelog = [
'Existing enabled legacy configs are disabled so the main feed is not polluted by known-offline APIs.', 'Existing enabled legacy configs are disabled so the main feed is not polluted by known-offline APIs.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.0.1', version: '1.0.1',
title: 'Mobile polish', title: 'Mobile polish',
bullets: [ bullets: [
@@ -494,7 +571,7 @@ const lunarisChangelog = [
'Tag chip input no longer jumps while typing.', 'Tag chip input no longer jumps while typing.',
], ],
), ),
LunarisChange( PrismaChange(
version: '1.0.0', version: '1.0.0',
title: 'Stable 1.0', title: 'Stable 1.0',
bullets: [ bullets: [
@@ -504,7 +581,7 @@ const lunarisChangelog = [
'Windows portable package is published alongside APK and installer.', 'Windows portable package is published alongside APK and installer.',
], ],
), ),
LunarisChange( PrismaChange(
version: '0.4.3', version: '0.4.3',
title: 'Bugfixes', title: 'Bugfixes',
bullets: [ bullets: [
@@ -516,8 +593,8 @@ const lunarisChangelog = [
), ),
]; ];
class LunarisChange { class PrismaChange {
const LunarisChange({ const PrismaChange({
required this.version, required this.version,
required this.title, required this.title,
required this.bullets, required this.bullets,
@@ -527,3 +604,6 @@ class LunarisChange {
final String title; final String title;
final List<String> bullets; final List<String> bullets;
} }
const lunarisChangelog = prismaChangelog;
typedef LunarisChange = PrismaChange;
+14 -4
View File
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AppBreakpoints { class AppBreakpoints {
@@ -6,11 +8,19 @@ class AppBreakpoints {
} }
class Responsive { class Responsive {
static bool isMobile(BuildContext context) => static bool isMobile(BuildContext context) {
MediaQuery.sizeOf(context).width < AppBreakpoints.mobile; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide < 600;
}
return MediaQuery.sizeOf(context).width < AppBreakpoints.mobile;
}
static bool isDesktop(BuildContext context) => static bool isDesktop(BuildContext context) {
MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop; if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide >= 600;
}
return MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop;
}
static int columnsFor( static int columnsFor(
BuildContext context, { BuildContext context, {
+1 -1
View File
@@ -224,7 +224,7 @@ final updateServiceProvider = Provider<UpdateService>((ref) {
connectTimeout: const Duration(seconds: 12), connectTimeout: const Duration(seconds: 12),
receiveTimeout: const Duration(seconds: 12), receiveTimeout: const Duration(seconds: 12),
headers: const { headers: const {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': 'application/json', 'Accept': 'application/json',
}, },
), ),
+11 -1
View File
@@ -60,11 +60,21 @@ class Post {
return null; return null;
} }
String? get description => commentary;
String? get title {
final list = tagGroups['copyright'];
if (list != null && list.isNotEmpty) {
return list.first;
}
return null;
}
List<String> get cleanTags { List<String> get cleanTags {
return tags.where((tag) { return tags.where((tag) {
final t = tag.trim(); final t = tag.trim();
if (t.isEmpty) return false; if (t.isEmpty) return false;
if (t == 'cloud_mirror') return false; if (t == 'cloud_mirror' || t == 'text_post') return false;
if (t.startsWith('{') && t.endsWith('}')) return false; if (t.startsWith('{') && t.endsWith('}')) return false;
if (t.contains('"url"') || t.contains('"service"')) return false; if (t.contains('"url"') || t.contains('"service"')) return false;
if (t.startsWith('http://') || t.startsWith('https://')) return false; if (t.startsWith('http://') || t.startsWith('https://')) return false;
+1 -1
View File
@@ -46,7 +46,7 @@ class GelbooruProvider
Map<String, String> mediaHeaders(Post post) { Map<String, String> mediaHeaders(Post post) {
final host = Uri.tryParse(baseUrl)?.host; final host = Uri.tryParse(baseUrl)?.host;
return { return {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*', 'Accept': '*/*',
if (host != null && host.isNotEmpty) 'Referer': '$baseUrl/', if (host != null && host.isNotEmpty) 'Referer': '$baseUrl/',
}; };
+34 -8
View File
@@ -30,7 +30,7 @@ class KemonoProvider
receiveTimeout: const Duration(seconds: 20), receiveTimeout: const Duration(seconds: 20),
headers: const { headers: const {
'Accept': 'application/json', 'Accept': 'application/json',
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
}, },
), ),
); );
@@ -230,8 +230,23 @@ class KemonoProvider
@override @override
Future<List<ArtistAnnouncement>> getArtistAnnouncements( Future<List<ArtistAnnouncement>> getArtistAnnouncements(
String service, String artistId) async => String service, String artistId) async {
const []; final path = '/api/v1/$service/user/$artistId/announcements';
for (final apiBase in _apiBaseCandidates()) {
try {
final response = await _getFromApiBase<dynamic>(apiBase, path);
final items = response.data;
if (items is List) {
return items
.whereType<Map>()
.map((item) =>
ArtistAnnouncement.fromJson(Map<String, dynamic>.from(item)))
.toList(growable: false);
}
} catch (_) {}
}
return const [];
}
String _creatorPath() => _isCoomer ? '/coomer' : '/kemono'; String _creatorPath() => _isCoomer ? '/coomer' : '/kemono';
@@ -385,17 +400,23 @@ class KemonoProvider
} }
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.) // Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
if (files.isEmpty && (cloudLinks.isNotEmpty || cleanDesc.isNotEmpty)) { // or text-only posts where author wrote an announcement, status or note
if (files.isEmpty &&
(cloudLinks.isNotEmpty ||
cleanDesc.isNotEmpty ||
rawContent.trim().isNotEmpty ||
title.trim().isNotEmpty)) {
final stableId = '${query.service}:${query.artistId}:$postId:0'; final stableId = '${query.service}:${query.artistId}:$postId:0';
final firstStreamable = final firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull; cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null; final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? ''; final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final isTextOnly = cloudLinks.isEmpty;
final tags = [ final tags = [
query.service, query.service,
query.artistName, query.artistName,
if (title.trim().isNotEmpty) title.trim(), if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror', if (!isTextOnly) 'cloud_mirror' else 'text_post',
]; ];
return [ return [
Post( Post(
@@ -411,14 +432,19 @@ class KemonoProvider
height: 0, height: 0,
source: postSource, source: postSource,
createdAt: published, createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link', fileType: firstStreamable != null
? 'video'
: (isTextOnly ? 'text' : 'link'),
score: 0, score: 0,
tagGroups: { tagGroups: {
'artist': [query.artistName], 'artist': [query.artistName],
'meta': [query.service], 'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()], if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson, if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc], if (cleanDesc.isNotEmpty)
'description': [cleanDesc]
else if (rawContent.trim().isNotEmpty)
'description': [rawContent.trim()],
}, },
), ),
]; ];
@@ -556,7 +582,7 @@ class KemonoProvider
receiveTimeout: const Duration(seconds: 25), receiveTimeout: const Duration(seconds: 25),
headers: const { headers: const {
'Accept': 'application/json', 'Accept': 'application/json',
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
}, },
), ),
); );
+15 -4
View File
@@ -418,17 +418,23 @@ class PawchiveProvider
} }
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.) // Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
if (files.isEmpty && (cloudLinks.isNotEmpty || cleanDesc.isNotEmpty)) { // or text-only posts where author wrote an announcement, status or note
if (files.isEmpty &&
(cloudLinks.isNotEmpty ||
cleanDesc.isNotEmpty ||
rawContent.trim().isNotEmpty ||
title.trim().isNotEmpty)) {
final stableId = '${query.service}:${query.artistId}:$postId:0'; final stableId = '${query.service}:${query.artistId}:$postId:0';
final firstStreamable = final firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull; cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null; final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? ''; final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final isTextOnly = cloudLinks.isEmpty;
final tags = [ final tags = [
query.service, query.service,
query.artistName, query.artistName,
if (title.trim().isNotEmpty) title.trim(), if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror', if (!isTextOnly) 'cloud_mirror' else 'text_post',
]; ];
return [ return [
Post( Post(
@@ -444,14 +450,19 @@ class PawchiveProvider
height: 0, height: 0,
source: postSource, source: postSource,
createdAt: published, createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link', fileType: firstStreamable != null
? 'video'
: (isTextOnly ? 'text' : 'link'),
score: 0, score: 0,
tagGroups: { tagGroups: {
'artist': [query.artistName], 'artist': [query.artistName],
'meta': [query.service], 'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()], if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson, if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc], if (cleanDesc.isNotEmpty)
'description': [cleanDesc]
else if (rawContent.trim().isNotEmpty)
'description': [rawContent.trim()],
}, },
), ),
]; ];
+1 -1
View File
@@ -569,7 +569,7 @@ class ProviderManager {
); );
} }
return const Success({ return const Success({
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*', 'Accept': '*/*',
}); });
} }
+1 -1
View File
@@ -21,7 +21,7 @@ class Rule34Provider extends GelbooruProvider {
@override @override
Map<String, String> mediaHeaders(Post post) => const { Map<String, String> mediaHeaders(Post post) => const {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*', 'Accept': '*/*',
'Referer': 'https://rule34.xxx/', 'Referer': 'https://rule34.xxx/',
}; };
@@ -60,7 +60,7 @@ class ProviderRepository {
priority: 3, priority: 3,
timeoutSeconds: 20, timeoutSeconds: 20,
customHeaders: const { customHeaders: const {
'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.2 Flutter local booru browser',
}, },
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
@@ -74,7 +74,7 @@ class ProviderRepository {
priority: 4, priority: 4,
timeoutSeconds: 20, timeoutSeconds: 20,
customHeaders: const { customHeaders: const {
'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.2 Flutter local booru browser',
}, },
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
+2 -2
View File
@@ -52,13 +52,13 @@ class BackupService {
Future<bool> exportBackup() async { Future<bool> exportBackup() async {
final json = await createBackupJson(); final json = await createBackupJson();
final fileName = final fileName =
'lunaris_backup_${DateTime.now().millisecondsSinceEpoch}.json'; 'prisma_backup_${DateTime.now().millisecondsSinceEpoch}.json';
if (Platform.isAndroid) { if (Platform.isAndroid) {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final file = File('${tempDir.path}/$fileName'); final file = File('${tempDir.path}/$fileName');
await file.writeAsString(json); await file.writeAsString(json);
await Share.shareXFiles([XFile(file.path)], text: 'Lunaris Backup'); await Share.shareXFiles([XFile(file.path)], text: 'Prisma Backup');
return true; return true;
} }
+1 -1
View File
@@ -224,7 +224,7 @@ class DownloadService {
return { return {
'User-Agent': lower.contains('realbooru') || lower.contains('paheal') 'User-Agent': lower.contains('realbooru') || lower.contains('paheal')
? 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/125 Mobile Safari/537.36' ? 'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/125 Mobile Safari/537.36'
: 'Lunaris/2.0.1 Flutter local booru browser', : 'Prisma/2.0.1 Flutter local booru browser',
'Accept': lower.contains('realbooru') || lower.contains('paheal') 'Accept': lower.contains('realbooru') || lower.contains('paheal')
? 'video/webm,video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8' ? 'video/webm,video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8'
: '*/*', : '*/*',
@@ -94,7 +94,7 @@ class PawchiveSyncService {
followRedirects: false, followRedirects: false,
validateStatus: (status) => status != null && status < 500, validateStatus: (status) => status != null && status < 500,
headers: const { headers: const {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}, },
), ),
@@ -231,7 +231,7 @@ class PawchiveSyncService {
headers: { headers: {
'Cookie': 'session=$cleanCookie', 'Cookie': 'session=$cleanCookie',
'Accept': 'application/json', 'Accept': 'application/json',
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
}, },
validateStatus: (status) => status != null && status < 500, validateStatus: (status) => status != null && status < 500,
), ),
@@ -509,7 +509,7 @@ class PawchiveSyncService {
final options = Options( final options = Options(
headers: { headers: {
'Cookie': 'session=$cleanCookie', 'Cookie': 'session=$cleanCookie',
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
}, },
validateStatus: (status) => validateStatus: (status) =>
status != null && (status == 200 || status == 201 || status == 204), status != null && (status == 200 || status == 201 || status == 204),
+1 -1
View File
@@ -674,7 +674,7 @@ class SettingsService {
return Success( return Success(
const JsonEncoder.withIndent(' ').convert({ const JsonEncoder.withIndent(' ').convert({
'app': { 'app': {
'name': 'Lunaris', 'name': 'Prisma',
'version': appVersion, 'version': appVersion,
'build': buildNumber, 'build': buildNumber,
'generatedAt': DateTime.now().toIso8601String(), 'generatedAt': DateTime.now().toIso8601String(),
+8 -1
View File
@@ -25,7 +25,14 @@ class TagCacheService {
return _resolvedPath!; return _resolvedPath!;
} }
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
_resolvedPath = '${dir.path}/lunaris_tag_cache.json'; final newFile = File('${dir.path}/prisma_tag_cache.json');
final oldFile = File('${dir.path}/lunaris_tag_cache.json');
if (!newFile.existsSync() && oldFile.existsSync()) {
try {
oldFile.copySync(newFile.path);
} catch (_) {}
}
_resolvedPath = newFile.path;
return _resolvedPath!; return _resolvedPath!;
} }
+9 -9
View File
@@ -18,14 +18,14 @@ class UpdateService {
UpdateService(this._dio, this._settingsService); UpdateService(this._dio, this._settingsService);
static const giteaLatestReleaseUrl = static const giteaLatestReleaseUrl =
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases/latest'; 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Prisma/releases/latest';
static const giteaReleasesUrl = static const giteaReleasesUrl =
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases'; 'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Prisma/releases';
static const githubLatestReleaseUrl = static const githubLatestReleaseUrl =
'https://api.github.com/repos/RarDog/Lunaris/releases/latest'; 'https://api.github.com/repos/RarDog/Prisma/releases/latest';
static const githubReleasesUrl = static const githubReleasesUrl =
'https://api.github.com/repos/RarDog/Lunaris/releases'; 'https://api.github.com/repos/RarDog/Prisma/releases';
static const latestReleaseUrl = githubLatestReleaseUrl; static const latestReleaseUrl = githubLatestReleaseUrl;
static const releasesUrl = githubReleasesUrl; static const releasesUrl = githubReleasesUrl;
@@ -111,7 +111,7 @@ class UpdateService {
'Authorization': 'token 1c744d6044d756759d7b1f693c94c80cf70d75fa', 'Authorization': 'token 1c744d6044d756759d7b1f693c94c80cf70d75fa',
} }
: { : {
'User-Agent': 'Lunaris-App', 'User-Agent': 'Prisma-App',
'Accept': 'application/vnd.github+json', 'Accept': 'application/vnd.github+json',
}, },
); );
@@ -239,10 +239,10 @@ class UpdateService {
final fromUrl = final fromUrl =
parsed?.pathSegments.isEmpty ?? true ? '' : parsed!.pathSegments.last; parsed?.pathSegments.isEmpty ?? true ? '' : parsed!.pathSegments.last;
if (fromUrl.contains('.')) return fromUrl; if (fromUrl.contains('.')) return fromUrl;
if (Platform.isAndroid) return 'Lunaris-v${info.version}.apk'; if (Platform.isAndroid) return 'Prisma-v${info.version}.apk';
if (Platform.isLinux) return 'Lunaris-v${info.version}-linux-x64.tar.gz'; if (Platform.isLinux) return 'Prisma-v${info.version}-linux-x64.tar.gz';
if (Platform.isWindows) return 'LunarisSetup-v${info.version}.exe'; if (Platform.isWindows) return 'PrismaSetup-v${info.version}.exe';
return 'LunarisPortable-v${info.version}.zip'; return 'PrismaPortable-v${info.version}.zip';
} }
static String _versionFromTag(String tag) { static String _versionFromTag(String tag) {
+1 -1
View File
@@ -40,7 +40,7 @@ class DioClient {
receiveTimeout: timeout, receiveTimeout: timeout,
sendTimeout: timeout, sendTimeout: timeout,
headers: { headers: {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': 'application/json, text/xml;q=0.9, */*;q=0.8', 'Accept': 'application/json, text/xml;q=0.9, */*;q=0.8',
...?headers, ...?headers,
}, },
@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.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:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -191,11 +192,13 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
if (_selectedTypes.isEmpty) return true; if (_selectedTypes.isEmpty) return true;
final isVid = MediaUrlSelector.isVideo(post); final isVid = MediaUrlSelector.isVideo(post);
final isGif = MediaUrlSelector.isGif(post); final isGif = MediaUrlSelector.isGif(post);
final isPhoto = !isVid && !isGif; final isText = post.fileType == 'text';
final isPhoto = !isVid && !isGif && !isText;
if (_selectedTypes.contains('video') && isVid) return true; if (_selectedTypes.contains('video') && isVid) return true;
if (_selectedTypes.contains('gif') && isGif) return true; if (_selectedTypes.contains('gif') && isGif) return true;
if (_selectedTypes.contains('photo') && isPhoto) return true; if (_selectedTypes.contains('photo') && isPhoto) return true;
if (_selectedTypes.contains('text') && isText) return true;
return false; return false;
} }
@@ -205,94 +208,251 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
showDragHandle: true, showDragHandle: true,
backgroundColor: theme.colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => DraggableScrollableSheet( builder: (ctx) => DraggableScrollableSheet(
expand: false, expand: false,
initialChildSize: 0.6, initialChildSize: 0.65,
maxChildSize: 0.9, maxChildSize: 0.92,
builder: (_, scroll) => Column( builder: (_, scroll) => Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), padding: const EdgeInsets.fromLTRB(20, 4, 16, 12),
child: Row( child: Row(
children: [ children: [
Icon(Icons.campaign_rounded, color: theme.colorScheme.primary), Container(
const SizedBox(width: 10), width: 44,
Text( height: 44,
'Анонсы автора (${_announcements.length})', decoration: BoxDecoration(
style: theme.textTheme.titleMedium?.copyWith( color: theme.colorScheme.primaryContainer,
fontWeight: FontWeight.bold, borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.campaign_rounded,
color: theme.colorScheme.onPrimaryContainer,
size: 24,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Личные сообщения и анонсы',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
),
),
const SizedBox(height: 2),
Text(
'${widget.artistName} • ${widget.service.toUpperCase()}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_announcements.length}',
style: TextStyle(
color: theme.colorScheme.onSecondaryContainer,
fontWeight: FontWeight.bold,
fontSize: 12,
),
), ),
), ),
], ],
), ),
), ),
const Divider(), Divider(
height: 1,
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4),
),
Expanded( Expanded(
child: ListView.separated( child: _announcements.isEmpty
controller: scroll, ? Center(
padding: const EdgeInsets.all(16), child: Column(
itemCount: _announcements.length, mainAxisSize: MainAxisSize.min,
separatorBuilder: (_, __) => const Divider(height: 24), children: [
itemBuilder: (context, index) { Icon(
final a = _announcements[index]; Icons.mark_email_read_outlined,
final cleanContent = CloudLinkExtractor.cleanCommentary(a.content); size: 48,
final detectedCloudLinks = CloudLinkExtractor.extractLinks(content: a.content); color: theme.colorScheme.outline,
final creatorLinks = CreatorLink.extractLinks(a.content);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (a.added != null)
Text(
'${a.added!.year}-${a.added!.month.toString().padLeft(2, '0')}-${a.added!.day.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
), ),
), const SizedBox(height: 12),
const SizedBox(height: 6), Text(
FormattedContentText( 'Нет сообщений или рассылок',
text: cleanContent.isNotEmpty ? cleanContent : a.content, style: theme.textTheme.titleSmall?.copyWith(
style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant,
height: 1.45, fontWeight: FontWeight.w600,
), ),
),
],
), ),
if (creatorLinks.isNotEmpty) ...[ )
const SizedBox(height: 10), : ListView.separated(
CreatorLinkChips( controller: scroll,
links: creatorLinks, padding: EdgeInsets.fromLTRB(
title: 'Ссылки автора', 16,
), 16,
], 16,
if (detectedCloudLinks.isNotEmpty) ...[ 120 + MediaQuery.paddingOf(context).bottom,
const SizedBox(height: 10), ),
Wrap( itemCount: _announcements.length,
spacing: 8, separatorBuilder: (_, __) => const SizedBox(height: 12),
runSpacing: 6, itemBuilder: (context, index) {
children: detectedCloudLinks.map((link) { final a = _announcements[index];
return ActionChip( final cleanContent =
avatar: Icon( CloudLinkExtractor.cleanCommentary(a.content);
link.iconData, final detectedCloudLinks =
size: 16, CloudLinkExtractor.extractLinks(content: a.content);
color: link.brandColor, final creatorLinks =
CreatorLink.extractLinks(a.content);
final displayContent = cleanContent.isNotEmpty
? cleanContent
: a.content;
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: theme.colorScheme.outlineVariant
.withValues(alpha: 0.45),
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(
(a.service.isNotEmpty
? a.service
: widget.service)
.toUpperCase(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
letterSpacing: 0.5,
),
),
),
if (a.added != null) ...[
const SizedBox(width: 8),
Icon(
Icons.access_time_rounded,
size: 13,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
'${a.added!.day.toString().padLeft(2, '0')}.${a.added!.month.toString().padLeft(2, '0')}.${a.added!.year} ${a.added!.hour.toString().padLeft(2, '0')}:${a.added!.minute.toString().padLeft(2, '0')}',
style: TextStyle(
fontSize: 11.5,
color:
theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
const Spacer(),
IconButton(
icon: const Icon(Icons.copy_rounded,
size: 17),
tooltip: 'Скопировать текст',
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
Clipboard.setData(
ClipboardData(text: displayContent),
);
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content:
Text('Текст сообщения скопирован'),
duration: Duration(seconds: 2),
),
);
},
),
],
), ),
label: Text( const SizedBox(height: 10),
link.title.isNotEmpty ? link.title : link.serviceName, FormattedContentText(
style: const TextStyle(fontSize: 12), text: displayContent,
style: theme.textTheme.bodyMedium?.copyWith(
height: 1.5,
letterSpacing: 0.15,
),
), ),
onPressed: () => launchUrl( if (creatorLinks.isNotEmpty) ...[
Uri.parse(link.url), const SizedBox(height: 12),
mode: LaunchMode.externalApplication, CreatorLinkChips(
), links: creatorLinks,
); title: 'Ссылки из сообщения',
}).toList(), ),
), ],
], if (detectedCloudLinks.isNotEmpty) ...[
], const SizedBox(height: 12),
); Wrap(
}, spacing: 8,
), runSpacing: 6,
children: detectedCloudLinks.map((link) {
return ActionChip(
avatar: Icon(
link.iconData,
size: 16,
color: link.brandColor,
),
label: Text(
link.title.isNotEmpty
? link.title
: link.serviceName,
style: const TextStyle(fontSize: 12),
),
onPressed: () => launchUrl(
Uri.parse(link.url),
mode: LaunchMode.externalApplication,
),
);
}).toList(),
),
],
],
),
);
},
),
), ),
], ],
), ),
@@ -303,10 +463,14 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
Widget _buildMediaTypeFilters() { Widget _buildMediaTypeFilters() {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final photoCount = _posts final photoCount = _posts
.where((p) => !MediaUrlSelector.isVideo(p) && !MediaUrlSelector.isGif(p)) .where((p) =>
p.fileType != 'text' &&
!MediaUrlSelector.isVideo(p) &&
!MediaUrlSelector.isGif(p))
.length; .length;
final videoCount = _posts.where(MediaUrlSelector.isVideo).length; final videoCount = _posts.where(MediaUrlSelector.isVideo).length;
final gifCount = _posts.where(MediaUrlSelector.isGif).length; final gifCount = _posts.where(MediaUrlSelector.isGif).length;
final textCount = _posts.where((p) => p.fileType == 'text').length;
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4), padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
@@ -376,6 +540,27 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
}); });
}, },
), ),
if (textCount > 0) ...[
const SizedBox(width: 8),
FilterChip(
selected: _selectedTypes.contains('text'),
avatar: Icon(Icons.article_outlined,
size: 16,
color: _selectedTypes.contains('text')
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
label: Text('Текст ($textCount)'),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTypes.add('text');
} else {
_selectedTypes.remove('text');
}
});
},
),
],
], ],
), ),
), ),
@@ -653,7 +653,7 @@ class _FeedTitle extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'Lunaris', 'Prisma',
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ import '../../../../backend/models/cloud_media_link.dart';
import '../../../../backend/models/creator_link.dart'; import '../../../../backend/models/creator_link.dart';
import '../../../../shared/widgets/formatted_content_text.dart'; import '../../../../shared/widgets/formatted_content_text.dart';
class CloudMirrorsCard extends StatelessWidget { class CloudMirrorsCard extends StatefulWidget {
const CloudMirrorsCard({ const CloudMirrorsCard({
super.key, super.key,
required this.links, required this.links,
@@ -25,9 +25,21 @@ class CloudMirrorsCard extends StatelessWidget {
final ValueChanged<String>? onDownloadStream; final ValueChanged<String>? onDownloadStream;
final bool initiallyExpanded; final bool initiallyExpanded;
@override
State<CloudMirrorsCard> createState() => _CloudMirrorsCardState();
}
class _CloudMirrorsCardState extends State<CloudMirrorsCard> {
int _selectedTab = 0; // 0: mirrors, 1: commentary
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (links.isEmpty && (commentary == null || commentary!.trim().isEmpty)) { final links = widget.links;
final commentary = widget.commentary;
final hasLinks = links.isNotEmpty;
final hasCommentary = commentary != null && commentary.trim().isNotEmpty;
if (!hasLinks && !hasCommentary) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -37,6 +49,14 @@ class CloudMirrorsCard extends StatelessWidget {
.whereType<String>() .whereType<String>()
.firstWhere((p) => p.isNotEmpty, orElse: () => ''); .firstWhere((p) => p.isNotEmpty, orElse: () => '');
final cardTitle = hasLinks
? (widget.strings.ru
? 'Облачные диски и зеркала (${links.length})'
: 'Cloud Mirrors & Drives (${links.length})')
: (widget.strings.ru ? 'Описание от автора' : 'Artist Commentary');
final cardIcon = hasLinks ? Icons.cloud_sync_rounded : Icons.notes_rounded;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow, color: theme.colorScheme.surfaceContainerLow,
@@ -49,20 +69,18 @@ class CloudMirrorsCard extends StatelessWidget {
child: ExpansionTile( child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), tilePadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 14), childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
initiallyExpanded: initiallyExpanded, initiallyExpanded: widget.initiallyExpanded,
shape: const Border(), shape: const Border(),
collapsedShape: const Border(), collapsedShape: const Border(),
leading: Icon( leading: Icon(
Icons.cloud_sync_rounded, cardIcon,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
title: Row( title: Row(
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
strings.ru cardTitle,
? 'Облачные диски и зеркала (${links.length})'
: 'Cloud Mirrors & Drives (${links.length})',
style: theme.textTheme.titleSmall?.copyWith( style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -77,7 +95,8 @@ class CloudMirrorsCard extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.15), color: Colors.green.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.withValues(alpha: 0.4)), border:
Border.all(color: Colors.green.withValues(alpha: 0.4)),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -86,7 +105,7 @@ class CloudMirrorsCard extends StatelessWidget {
size: 13, color: Colors.green), size: 13, color: Colors.green),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
strings.ru ? 'Плеер' : 'Stream', widget.strings.ru ? 'Плеер' : 'Stream',
style: theme.textTheme.labelSmall?.copyWith( style: theme.textTheme.labelSmall?.copyWith(
color: Colors.green, color: Colors.green,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -116,7 +135,9 @@ class CloudMirrorsCard extends StatelessWidget {
Expanded( Expanded(
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
text: strings.ru ? 'Пароль к архиву: ' : 'Archive Password: ', text: widget.strings.ru
? 'Пароль к архиву: '
: 'Archive Password: ',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
@@ -135,13 +156,15 @@ class CloudMirrorsCard extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.copy_rounded, size: 16), icon: const Icon(Icons.copy_rounded, size: 16),
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
tooltip: strings.ru ? 'Копировать пароль' : 'Copy Password', tooltip: widget.strings.ru
? 'Копировать пароль'
: 'Copy Password',
onPressed: () { onPressed: () {
Clipboard.setData(ClipboardData(text: detectedPassword)); Clipboard.setData(ClipboardData(text: detectedPassword));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
strings.ru widget.strings.ru
? 'Пароль скопирован: $detectedPassword' ? 'Пароль скопирован: $detectedPassword'
: 'Password copied: $detectedPassword', : 'Password copied: $detectedPassword',
), ),
@@ -157,63 +180,111 @@ class CloudMirrorsCard extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
], ],
// Links list if (hasLinks && hasCommentary) ...[
if (links.isNotEmpty) ...[ Padding(
...links.map((link) => _buildLinkTile(context, link, strings)), padding: const EdgeInsets.only(bottom: 12),
], child: SizedBox(
width: double.infinity,
// Commentary / Description child: SegmentedButton<int>(
if (commentary != null && commentary!.trim().isNotEmpty) ...[ segments: [
const SizedBox(height: 10), ButtonSegment(
Divider( value: 0,
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4), icon: const Icon(Icons.cloud_queue_rounded, size: 16),
height: 1, label: Text(
), widget.strings.ru
const SizedBox(height: 10), ? 'Зеркала (${links.length})'
Row( : 'Mirrors (${links.length})',
children: [ ),
Icon( ),
Icons.notes_rounded, ButtonSegment(
size: 16, value: 1,
color: theme.colorScheme.primary, icon: const Icon(Icons.notes_rounded, size: 16),
label: Text(
widget.strings.ru ? 'Описание' : 'Description',
),
),
],
selected: {_selectedTab},
onSelectionChanged: (set) =>
setState(() => _selectedTab = set.first),
), ),
const SizedBox(width: 6),
Text(
strings.ru ? 'Описание от автора' : 'Artist Commentary',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
),
const SizedBox(height: 8),
FormattedContentText(
text: commentary!.trim(),
style: theme.textTheme.bodySmall?.copyWith(
height: 1.45,
color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
Builder( IndexedStack(
builder: (context) { index: _selectedTab,
final creatorLinks = CreatorLink.extractLinks(commentary); children: [
if (creatorLinks.isEmpty) return const SizedBox.shrink(); Column(
return Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.only(top: 10), children: links
child: CreatorLinkChips( .map((link) => _buildLinkTile(
links: creatorLinks, context,
title: strings.ru ? 'Ссылки автора' : 'Author Links', link,
), widget.strings,
); ))
}, .toList(),
),
_buildCommentarySection(context, commentary, theme),
],
), ),
] else if (hasLinks) ...[
...links.map((link) => _buildLinkTile(
context,
link,
widget.strings,
)),
] else if (hasCommentary) ...[
_buildCommentarySection(context, commentary, theme),
], ],
], ],
), ),
); );
} }
Widget _buildCommentarySection(
BuildContext context,
String text,
ThemeData theme,
) {
final creatorLinks = CreatorLink.extractLinks(text);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.notes_rounded,
size: 16,
color: theme.colorScheme.primary,
),
const SizedBox(width: 6),
Text(
widget.strings.ru ? 'Описание от автора' : 'Artist Commentary',
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
],
),
const SizedBox(height: 8),
FormattedContentText(
text: text.trim(),
style: theme.textTheme.bodySmall?.copyWith(
height: 1.45,
color: theme.colorScheme.onSurfaceVariant,
),
),
if (creatorLinks.isNotEmpty) ...[
const SizedBox(height: 10),
CreatorLinkChips(
links: creatorLinks,
title: widget.strings.ru ? 'Ссылки автора' : 'Author Links',
),
],
],
);
}
Widget _buildLinkTile( Widget _buildLinkTile(
BuildContext context, BuildContext context,
CloudMediaLink link, CloudMediaLink link,
@@ -303,7 +374,7 @@ class CloudMirrorsCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
// Play button (if streamable) // Play button (if streamable)
if (link.isStreamable && onPlayStream != null) ...[ if (link.isStreamable && widget.onPlayStream != null) ...[
FilledButton.tonalIcon( FilledButton.tonalIcon(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -313,7 +384,7 @@ class CloudMirrorsCard extends StatelessWidget {
label: Text(strings.ru ? 'Смотреть' : 'Play'), label: Text(strings.ru ? 'Смотреть' : 'Play'),
onPressed: () { onPressed: () {
final target = link.directStreamUrl ?? link.url; final target = link.directStreamUrl ?? link.url;
onPlayStream!(target); widget.onPlayStream!(target);
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -321,12 +392,13 @@ class CloudMirrorsCard extends StatelessWidget {
// Download button (if direct streamable and callback exists) // Download button (if direct streamable and callback exists)
if (link.isStreamable && if (link.isStreamable &&
link.directStreamUrl != null && link.directStreamUrl != null &&
onDownloadStream != null) ...[ widget.onDownloadStream != null) ...[
IconButton.filledTonal( IconButton.filledTonal(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
tooltip: strings.download, tooltip: strings.download,
icon: const Icon(Icons.download_rounded, size: 16), icon: const Icon(Icons.download_rounded, size: 16),
onPressed: () => onDownloadStream!(link.directStreamUrl!), onPressed: () =>
widget.onDownloadStream!(link.directStreamUrl!),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
File diff suppressed because it is too large Load Diff
@@ -494,7 +494,7 @@ class _ProviderFormScreenState extends ConsumerState<ProviderFormScreen> {
child: _GlassTextField( child: _GlassTextField(
controller: _headers, controller: _headers,
label: isRu ? 'Пользовательские заголовки' : 'Headers JSON', label: isRu ? 'Пользовательские заголовки' : 'Headers JSON',
hint: '{"User-Agent": "Lunaris/3.0"}', hint: '{"User-Agent": "Prisma/3.0"}',
icon: Icons.code_rounded, icon: Icons.code_rounded,
minLines: 2, minLines: 2,
maxLines: 5, maxLines: 5,
@@ -403,7 +403,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
? 'Beta и экспериментальные обновления' ? 'Beta и экспериментальные обновления'
: 'Receive beta & experimental updates', : 'Receive beta & experimental updates',
subtitle: isRu subtitle: isRu
? 'Проверка и уведомление о prerelease сборках Lunaris' ? 'Проверка и уведомление о prerelease сборках Prisma'
: 'Include prerelease builds when checking for updates', : 'Include prerelease builds when checking for updates',
value: settings.allowExperimentalUpdates, value: settings.allowExperimentalUpdates,
onChanged: (val) => _update( onChanged: (val) => _update(
@@ -1278,7 +1278,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
messenger.showSnackBar( messenger.showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
isRu ? 'У вас установлена последняя версия Lunaris' : 'Lunaris is up to date', isRu ? 'У вас установлена последняя версия Prisma' : 'Prisma is up to date',
), ),
), ),
); );
@@ -1295,7 +1295,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
await showDialog<void>( await showDialog<void>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text('Lunaris ${info.version} is available'), title: Text('Prisma ${info.version} is available'),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520), constraints: const BoxConstraints(maxWidth: 520),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -1356,7 +1356,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
return showDialog<void>( return showDialog<void>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Lunaris changelog'), title: const Text('Prisma changelog'),
content: ConstrainedBox( content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560), constraints: const BoxConstraints(maxWidth: 560),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -1364,7 +1364,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
for (final change in lunarisChangelog) ...[ for (final change in prismaChangelog) ...[
Text( Text(
'${change.version} - ${change.title}', '${change.version} - ${change.title}',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
@@ -2341,7 +2341,7 @@ class _ColorSwatches extends StatelessWidget {
final ValueChanged<int> onChanged; final ValueChanged<int> onChanged;
static const colors = <(int, String)>[ static const colors = <(int, String)>[
(0xFFE84D8A, 'Lunaris pink'), (0xFFE84D8A, 'Prisma pink'),
(0xFF8B5CF6, 'Violet'), (0xFF8B5CF6, 'Violet'),
(0xFF3B82F6, 'Azure'), (0xFF3B82F6, 'Azure'),
(0xFF0EA5E9, 'Sky'), (0xFF0EA5E9, 'Sky'),
@@ -2565,7 +2565,7 @@ class _HeroBrandBanner extends StatelessWidget {
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
Text( Text(
'Lunaris', 'Prisma',
style: theme.textTheme.headlineSmall?.copyWith( style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
letterSpacing: -0.5, letterSpacing: -0.5,
@@ -2619,7 +2619,7 @@ class _HeroBrandBanner extends StatelessWidget {
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () async { onPressed: () async {
final url = Uri.parse( final url = Uri.parse(
'https://github.com/RarDog/Lunaris', 'https://github.com/RarDog/Prisma',
); );
await launchUrl(url, mode: LaunchMode.externalApplication); await launchUrl(url, mode: LaunchMode.externalApplication);
}, },
+185 -22
View File
@@ -117,9 +117,14 @@ class _PostCardState extends ConsumerState<PostCard>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final post = _resolvedPost ?? widget.post; final post = _resolvedPost ?? widget.post;
final sensitive = _isSensitive(post.rating); final sensitive = _isSensitive(post.rating);
final isTextPost = post.fileType == 'text' ||
(post.previewUrl.trim().isEmpty &&
post.sampleUrl.trim().isEmpty &&
post.fileUrl.trim().isEmpty &&
!MediaUrlSelector.isVideo(post));
final aspect = post.width > 0 && post.height > 0 final aspect = post.width > 0 && post.height > 0
? post.width / post.height ? post.width / post.height
: _fallbackAspect(post.fileType); : _fallbackAspect(isTextPost ? 'text' : post.fileType);
final mobile = MediaQuery.sizeOf(context).width < 700; final mobile = MediaQuery.sizeOf(context).width < 700;
final urls = _feedUrls(post, mobile: mobile); final urls = _feedUrls(post, mobile: mobile);
final imageUrl = urls.isEmpty ? post.previewUrl : urls.first; final imageUrl = urls.isEmpty ? post.previewUrl : urls.first;
@@ -202,6 +207,12 @@ class _PostCardState extends ConsumerState<PostCard>
enabled: widget.blurExplicit && sensitive, enabled: widget.blurExplicit && sensitive,
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
if (isTextPost) {
return _TextPostCardPreview(
post: post,
hovered: _hovered && !mobile,
);
}
final dpr = MediaQuery.devicePixelRatioOf(context); final dpr = MediaQuery.devicePixelRatioOf(context);
final cacheWidth = final cacheWidth =
(constraints.maxWidth * dpr).round().clamp(320, 1800); (constraints.maxWidth * dpr).round().clamp(320, 1800);
@@ -239,27 +250,28 @@ class _PostCardState extends ConsumerState<PostCard>
), ),
), ),
), ),
// Bottom cinematic gradient overlay for badges and favorite // Bottom cinematic gradient overlay for badges and favorite (only on media posts)
Positioned( if (!isTextPost)
left: 0, Positioned(
right: 0, left: 0,
bottom: 0, right: 0,
height: 64, bottom: 0,
child: IgnorePointer( height: 64,
child: DecoratedBox( child: IgnorePointer(
decoration: BoxDecoration( child: DecoratedBox(
gradient: LinearGradient( decoration: BoxDecoration(
begin: Alignment.bottomCenter, gradient: LinearGradient(
end: Alignment.topCenter, begin: Alignment.bottomCenter,
colors: [ end: Alignment.topCenter,
Colors.black.withValues(alpha: 0.72), colors: [
Colors.transparent, Colors.black.withValues(alpha: 0.72),
], Colors.transparent,
],
),
), ),
), ),
), ),
), ),
),
if (widget.showBadges) ...[ if (widget.showBadges) ...[
Positioned( Positioned(
left: 8, left: 8,
@@ -269,11 +281,14 @@ class _PostCardState extends ConsumerState<PostCard>
if (post.fileType.toLowerCase().contains('video') || if (post.fileType.toLowerCase().contains('video') ||
post.fileType.toLowerCase().contains('webm') || post.fileType.toLowerCase().contains('webm') ||
post.fileType.toLowerCase().contains('mp4') || post.fileType.toLowerCase().contains('mp4') ||
post.fileType.toLowerCase().contains('gif')) post.fileType.toLowerCase().contains('gif') ||
isTextPost)
Positioned( Positioned(
right: 8, right: 8,
top: 8, top: 8,
child: _MediaBadge(fileType: post.fileType), child: _MediaBadge(
fileType: isTextPost ? 'text' : post.fileType,
),
), ),
Positioned( Positioned(
left: 8, left: 8,
@@ -434,6 +449,7 @@ class _PostCardState extends ConsumerState<PostCard>
double _fallbackAspect(String fileType) { double _fallbackAspect(String fileType) {
final normalized = fileType.toLowerCase(); final normalized = fileType.toLowerCase();
if (normalized == 'text') return 0.95;
if (normalized.contains('video') || normalized.contains('webm')) { if (normalized.contains('video') || normalized.contains('webm')) {
return 16 / 9; return 16 / 9;
} }
@@ -462,7 +478,7 @@ class _PostCardState extends ConsumerState<PostCard>
'${post.previewUrl} ${post.sampleUrl} ${post.fileUrl}' '${post.previewUrl} ${post.sampleUrl} ${post.fileUrl}'
.toLowerCase(); .toLowerCase();
return { return {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser', 'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*', 'Accept': '*/*',
if (lower.contains('gelbooru')) 'Referer': 'https://gelbooru.com/', if (lower.contains('gelbooru')) 'Referer': 'https://gelbooru.com/',
if (lower.contains('realbooru')) if (lower.contains('realbooru'))
@@ -596,6 +612,8 @@ class _FeedVideoPreviewState extends State<_FeedVideoPreview> {
controller: _controller, controller: _controller,
fit: BoxFit.cover, fit: BoxFit.cover,
controls: null, controls: null,
pauseUponEnteringBackgroundMode: false,
resumeUponEnteringForegroundMode: false,
), ),
), ),
), ),
@@ -773,7 +791,7 @@ class _MediaBadge extends StatelessWidget {
Icon(_icon(type), size: 13, color: Colors.white), Icon(_icon(type), size: 13, color: Colors.white),
const SizedBox(width: 3.5), const SizedBox(width: 3.5),
Text( Text(
type, _label(type),
style: const TextStyle( style: const TextStyle(
fontSize: 10.5, fontSize: 10.5,
color: Colors.white, color: Colors.white,
@@ -794,14 +812,159 @@ class _MediaBadge extends StatelessWidget {
return 'video'; return 'video';
} }
if (value.contains('gif')) return 'gif'; if (value.contains('gif')) return 'gif';
if (value.contains('text')) return 'text';
return 'photo'; return 'photo';
} }
IconData _icon(String type) => switch (type) { IconData _icon(String type) => switch (type) {
'video' => Icons.play_arrow_rounded, 'video' => Icons.play_arrow_rounded,
'gif' => Icons.gif_box_rounded, 'gif' => Icons.gif_box_rounded,
'text' => Icons.article_rounded,
_ => Icons.image_rounded, _ => Icons.image_rounded,
}; };
String _label(String type) => switch (type) {
'video' => 'VIDEO',
'gif' => 'GIF',
'text' => 'ТЕКСТ',
_ => 'PHOTO',
};
}
class _TextPostCardPreview extends StatelessWidget {
const _TextPostCardPreview({
required this.post,
required this.hovered,
});
final Post post;
final bool hovered;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final title = (post.title ?? '').trim();
final cleanContent =
CloudLinkExtractor.cleanCommentary(post.description ?? '');
final hasTitle = title.isNotEmpty;
final snippet = cleanContent.isNotEmpty
? cleanContent
: (hasTitle ? '' : 'Текстовая публикация автора без вложений');
final cloudCount = post.cloudLinks.length;
return Container(
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.surfaceContainerHigh,
theme.colorScheme.surfaceContainer,
],
),
border: Border.all(
color: hovered
? theme.colorScheme.primary.withValues(alpha: 0.5)
: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.14),
shape: BoxShape.circle,
),
child: Icon(
Icons.notes_rounded,
size: 15,
color: theme.colorScheme.primary,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${post.createdAt.day.toString().padLeft(2, '0')}.${post.createdAt.month.toString().padLeft(2, '0')}.${post.createdAt.year}',
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 10),
if (hasTitle) ...[
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
height: 1.25,
),
),
const SizedBox(height: 6),
],
if (snippet.isNotEmpty)
Expanded(
child: Text(
snippet,
maxLines: hasTitle ? 5 : 7,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
height: 1.42,
),
),
)
else
const Spacer(),
if (cloudCount > 0) ...[
const SizedBox(height: 6),
Row(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(6),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.cloud_download_outlined,
size: 12,
color: theme.colorScheme.onPrimaryContainer,
),
const SizedBox(width: 4),
Text(
'Вложения ($cloudCount)',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimaryContainer,
),
),
],
),
),
],
),
],
],
),
);
}
} }
class _SeenBadge extends StatelessWidget { class _SeenBadge extends StatelessWidget {
+2 -2
View File
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) { if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
gtk_widget_show(GTK_WIDGET(header_bar)); gtk_widget_show(GTK_WIDGET(header_bar));
gtk_header_bar_set_title(header_bar, "Lunaris"); gtk_header_bar_set_title(header_bar, "Prisma");
gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_header_bar_set_show_close_button(header_bar, TRUE);
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else { } else {
gtk_window_set_title(window, "Lunaris"); gtk_window_set_title(window, "Prisma");
} }
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);
+4 -4
View File
@@ -899,18 +899,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_android name: screen_brightness_android
sha256: "2df5e0488a22a73271a43d92dbcd80643df6910cf69690faacaf9929ae9ade3b" sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.6"
screen_brightness_platform_interface: screen_brightness_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_platform_interface name: screen_brightness_platform_interface
sha256: "59d50850d6735d677780fc7359c8e997d0ff6df91c8465161c9e617a7b0a11d8" sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
+1 -2
View File
@@ -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.1.6+67 version: 3.4.1+75
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"
@@ -33,7 +33,6 @@ dependencies:
media_kit_libs_android_video: ^1.3.8 media_kit_libs_android_video: ^1.3.8
share_plus: ^10.0.0 share_plus: ^10.0.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
+1 -1
View File
@@ -10,7 +10,7 @@ void main() {
late TagCacheService cacheService; late TagCacheService cacheService;
setUp(() async { setUp(() async {
tempDir = await Directory.systemTemp.createTemp('lunaris_test_tag_cache'); tempDir = await Directory.systemTemp.createTemp('prisma_test_tag_cache');
cacheFilePath = '${tempDir.path}/test_tags.json'; cacheFilePath = '${tempDir.path}/test_tags.json';
cacheService = TagCacheService(customPath: cacheFilePath); cacheService = TagCacheService(customPath: cacheFilePath);
await cacheService.init(); await cacheService.init();
+8 -8
View File
@@ -13,24 +13,24 @@ void main() {
final info = AppUpdateInfo( final info = AppUpdateInfo(
version: '2.0.2', version: '2.0.2',
tagName: 'v2.0.2', tagName: 'v2.0.2',
name: 'Lunaris 2.0.2', name: 'Prisma 2.0.2',
body: 'Release notes', body: 'Release notes',
htmlUrl: 'https://github.com/RarDog/Lunaris/releases/tag/v2.0.2', htmlUrl: 'https://github.com/RarDog/Prisma/releases/tag/v2.0.2',
publishedAt: DateTime.parse('2026-09-03T18:00:00Z'), publishedAt: DateTime.parse('2026-09-03T18:00:00Z'),
apkUrl: 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/Lunaris.apk', apkUrl: 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/Prisma.apk',
linuxTarGzUrl: 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/linux.tar.gz', linuxTarGzUrl: 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/linux.tar.gz',
); );
expect(info.hasAssets, isTrue); expect(info.hasAssets, isTrue);
expect(info.apkUrl, contains('apk')); expect(info.apkUrl, contains('apk'));
expect(info.linuxTarGzUrl, contains('linux')); expect(info.linuxTarGzUrl, contains('linux'));
expect( expect(
service.assetFileName(info, 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/Lunaris-v2.0.2.apk'), service.assetFileName(info, 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/Prisma-v2.0.2.apk'),
'Lunaris-v2.0.2.apk', 'Prisma-v2.0.2.apk',
); );
expect( expect(
service.assetFileName(info, 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/unknown-linux'), service.assetFileName(info, 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/unknown-linux'),
'Lunaris-v2.0.2-linux-x64.tar.gz', 'Prisma-v2.0.2-linux-x64.tar.gz',
); );
}); });
} }
+11 -5
View File
@@ -55,15 +55,21 @@ void main() {
expect(find.text('Google Drive'), findsOneWidget); expect(find.text('Google Drive'), findsOneWidget);
expect(find.text('Google Drive Stream'), findsOneWidget); expect(find.text('Google Drive Stream'), findsOneWidget);
// Verify commentary // Verify play button triggers callback on mirrors tab
expect(find.text('Описание от автора'), findsOneWidget);
expect(find.text('Special Patreon bonus animation for all patrons.'), findsOneWidget);
// Verify play button triggers callback
final playBtn = find.text('Смотреть'); final playBtn = find.text('Смотреть');
expect(playBtn, findsOneWidget); expect(playBtn, findsOneWidget);
await tester.tap(playBtn); await tester.tap(playBtn);
expect(playedStream, 'https://drive.google.com/uc?export=download&id=123'); expect(playedStream, 'https://drive.google.com/uc?export=download&id=123');
// Switch to description tab and verify commentary
final descTab = find.text('Описание');
expect(descTab, findsOneWidget);
await tester.tap(descTab);
await tester.pumpAndSettle();
// Verify commentary
expect(find.text('Описание от автора'), findsOneWidget);
expect(find.text('Special Patreon bonus animation for all patrons.'), findsOneWidget);
}); });
testWidgets('CloudMirrorsCard renders nothing if no links and no commentary', (tester) async { testWidgets('CloudMirrorsCard renders nothing if no links and no commentary', (tester) async {
+3 -3
View File
@@ -90,12 +90,12 @@ BEGIN
BLOCK "040904e4" BLOCK "040904e4"
BEGIN BEGIN
VALUE "CompanyName", "com.example" "\0" VALUE "CompanyName", "com.example" "\0"
VALUE "FileDescription", "Lunaris" "\0" VALUE "FileDescription", "Prisma" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "Lunaris" "\0" VALUE "InternalName", "Prisma" "\0"
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
VALUE "OriginalFilename", "gel_rule_app.exe" "\0" VALUE "OriginalFilename", "gel_rule_app.exe" "\0"
VALUE "ProductName", "Lunaris" "\0" VALUE "ProductName", "Prisma" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"
END END
END END
+1 -1
View File
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
FlutterWindow window(project); FlutterWindow window(project);
Win32Window::Point origin(10, 10); Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720); Win32Window::Size size(1280, 720);
if (!window.Create(L"Lunaris", origin, size)) { if (!window.Create(L"Prisma", origin, size)) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
window.SetQuitOnClose(true); window.SetQuitOnClose(true);