Compare commits

..
7 Commits
Author SHA1 Message Date
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
31 changed files with 1503 additions and 554 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.0-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.4.0" /></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 -1
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,
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.1.6'; const appDisplayVersion = '3.4.0';
const appBuildNumber = 67; const appBuildNumber = 74;
const appReleaseChannel = 'Stable'; const appReleaseChannel = 'Stable';
+68
View File
@@ -1,4 +1,72 @@
const lunarisChangelog = [ const lunarisChangelog = [
LunarisChange(
version: '3.4.0',
title: 'Ребрендинг в Prisma: новое имя, комфортная прокрутка анонсов и обновленные репозитории',
bullets: [
'Ребрендинг в Prisma: приложение официально переименовано в Prisma — обновлены названия окон, манифесты и системные идентификаторы.',
'Улучшенная прокрутка анонсов: в диалоге личных сообщений и рассылок автора добавлен дополнительный нижний отступ, чтобы карточки не перекрывались нижней панелью навигации и системными элементами управления.',
'Синхронизация репозиториев: репозитории на GitHub и Gitea переведены на Prisma, ссылки внутри приложения и служба автообновления переключены на новые адреса.',
],
),
LunarisChange(
version: '3.3.1',
title: 'Улучшенное разделение текстовых постов и облачных зеркал',
bullets: [
'Исключение дублирования в текстовых публикациях: для текстовых постов (без медиа) убрана лишняя плашка «Облачные диски и зеркала», исключая повторный вывод того же текста внизу экрана.',
'Вкладки «Зеркала» и «Описание»: для постов с фото, видео и GIF в блоке облачных дисков теперь доступны удобные переключаемые вкладки, позволяющие отдельно просматривать список ссылок на хранилища и авторское описание.',
'Чистый и нативный вид карточек: если у медиапоста есть только ссылки или только описание, отображается аккуратная монолитная секция с соответствующим заголовком и иконкой без лишних переключателей.',
],
),
LunarisChange(
version: '3.3.0',
title: 'Редизайн личных сообщений и рассылок артиста, нативная поддержка текстовых публикаций',
bullets: [
'Центр сообщений и рассылок автора: полностью переработан диалог по кнопке-рупору на экране артиста — аккуратные карточки сообщений в стиле мессенджера, бейджи сервисов, точное время публикации и кнопка быстрого копирования текста в буфер обмена.',
'Нативная поддержка текстовых постов: публикации без медиафайлов больше не вызывают ошибок и пустых плейсхолдеров — в сетке они отображаются как стильные заметки с заголовком и выдержкой текста.',
'Удобное чтение статей и заметок: при открытии текстового поста отображается комфортный ридер с форматированным текстом, ссылками автора и тегами.',
'Фильтрация по типу контента: на экране автора добавлен фильтр «Текст» с подсчетом количества публикаций наряду с фото, видео и GIF.',
'Поддержка анонсов в Kemono и Coomer: API анонсов и персональных сообщений теперь полноценно работает для всех поддерживаемых провайдеров.',
],
),
LunarisChange(
version: '3.2.0',
title: 'Стабильный полноэкранный режим: сохранение воспроизведения, устранение вылетов плеера и чистый интерфейс',
bullets: [
'Сохранение воспроизведения видео при повороте экрана: исправлена логика адаптивности на смартфонах — альбомная ориентация больше не сбрасывает мобильный интерфейс и не уничтожает запущенный плеер.',
'Защита жизненного цикла Player в фоне: плеер не уничтожается при переходе в полный экран, а кнопки «Пуск/Пауза» и перемотки работают мгновенно и безотказно.',
'Чистый кинематографичный интерфейс: удалены старые дублирующиеся кнопки в правом верхнем углу; кнопка смены ориентации (альбомный/портретный режим) аккуратно интегрирована в верхнюю панель управления.',
'Быстрое скрытие и показ контролов: касание свободной области видео плавно переключает видимость панели управления, а при нажатии на кнопки таймер автоскрытия автоматически обновляется.',
],
),
LunarisChange(
version: '3.1.9',
title: 'Полноэкранный плеер 2.0: мгновенный переход без задержек, жесты яркости и скраббинг с превью',
bullets: [
'Мгновенный переход в полноэкранный режим: убраны промежуточные задержки и наложения текстур, вход и выход происходят без малейших задержек.',
'Двухзонное жестовое управление: левая сторона экрана регулирует яркость подсветки с индикатором солнца, правая сторона — громкость звука.',
'Плавный скраббинг с плавающим таймингом: перемещение по таймлайну видео теперь отображает интерактивную плашку точного времени, а контролы не исчезают во время удержания ползунка.',
'Сброс яркости: при выходе из полноэкранного режима системный уровень яркости экрана аккуратно восстанавливается.',
],
),
LunarisChange(
version: '3.1.8',
title: 'Безупречный полноэкранный плеер: устранение остановки при развертывании и отзывчивые кнопки управления',
bullets: [
'Непрерывное воспроизведение при открытии на весь экран: видео больше не останавливается самопроизвольно при входе в полноэкранный режим или смене ориентации устройства.',
'Корректный статус воспроизведения: устранен сброс кнопки на «Пуск» при переходе в полный экран — видео продолжает воспроизведение без ложных остановок.',
'Мгновенный отклик кнопок «Пуск/Пауза» и перемотки: жесты управления поверхностью (двойной тап, смахивание громкости) перемещены в фоновый слой, исключая перехват и отмену нажатий по кнопкам управления.',
'Синхронизация состояния при сворачивании: плеер точно запоминает состояние паузы/воспроизведения при выходе из полноэкранного режима обратно в пост.',
],
),
LunarisChange(
version: '3.1.7',
title: 'Устранение критического сбоя при открытии видеопоста (SIGSEGV в libmpv)',
bullets: [
'Устранение нативного вылета: полностью удалены небезопасные прямые FFI-вызовы setProperty к mpv, приводившие к немедленному падению приложения на Android при инициализации видео.',
'Безопасная конфигурация декодирования: перевод настроек аппаратного ускорения и фоллбэка кодеков на официальный типобезопасный VideoControllerConfiguration (media_kit_video).',
'Надежный фоллбэк при редких ошибках кодека: при несовместимости аппаратного декодера плеер бесшовно переключается на программный FFmpeg-декодер без риска краша.',
],
),
LunarisChange( LunarisChange(
version: '3.1.6', version: '3.1.6',
title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека', title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека',
+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, {
+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;
+32 -6
View File
@@ -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()],
}, },
), ),
]; ];
+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()],
}, },
), ),
]; ];
+4 -4
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;
@@ -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,68 +208,222 @@ 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,
height: 44,
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
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( Text(
'Анонсы автора (${_announcements.length})', 'Личные сообщения и анонсы',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold, 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),
itemBuilder: (context, index) {
final a = _announcements[index];
final cleanContent = CloudLinkExtractor.cleanCommentary(a.content);
final detectedCloudLinks = CloudLinkExtractor.extractLinks(content: a.content);
final creatorLinks = CreatorLink.extractLinks(a.content);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (a.added != null) Icon(
Icons.mark_email_read_outlined,
size: 48,
color: theme.colorScheme.outline,
),
const SizedBox(height: 12),
Text( Text(
'${a.added!.year}-${a.added!.month.toString().padLeft(2, '0')}-${a.added!.day.toString().padLeft(2, '0')}', 'Нет сообщений или рассылок',
style: TextStyle( style: theme.textTheme.titleSmall?.copyWith(
fontSize: 12, color: theme.colorScheme.onSurfaceVariant,
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 6), ],
),
)
: ListView.separated(
controller: scroll,
padding: EdgeInsets.fromLTRB(
16,
16,
16,
120 + MediaQuery.paddingOf(context).bottom,
),
itemCount: _announcements.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final a = _announcements[index];
final cleanContent =
CloudLinkExtractor.cleanCommentary(a.content);
final detectedCloudLinks =
CloudLinkExtractor.extractLinks(content: a.content);
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),
),
);
},
),
],
),
const SizedBox(height: 10),
FormattedContentText( FormattedContentText(
text: cleanContent.isNotEmpty ? cleanContent : a.content, text: displayContent,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
height: 1.45, height: 1.5,
letterSpacing: 0.15,
), ),
), ),
if (creatorLinks.isNotEmpty) ...[ if (creatorLinks.isNotEmpty) ...[
const SizedBox(height: 10), const SizedBox(height: 12),
CreatorLinkChips( CreatorLinkChips(
links: creatorLinks, links: creatorLinks,
title: 'Ссылки автора', title: 'Ссылки из сообщения',
), ),
], ],
if (detectedCloudLinks.isNotEmpty) ...[ if (detectedCloudLinks.isNotEmpty) ...[
const SizedBox(height: 10), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 6, runSpacing: 6,
@@ -278,7 +435,9 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
color: link.brandColor, color: link.brandColor,
), ),
label: Text( label: Text(
link.title.isNotEmpty ? link.title : link.serviceName, link.title.isNotEmpty
? link.title
: link.serviceName,
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
onPressed: () => launchUrl( onPressed: () => launchUrl(
@@ -290,6 +449,7 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
), ),
], ],
], ],
),
); );
}, },
), ),
@@ -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');
}
});
},
),
],
], ],
), ),
), ),
@@ -110,6 +110,16 @@ class PostDetailsScreen extends ConsumerWidget {
t.status == DownloadTaskStatus.queued), t.status == DownloadTaskStatus.queued),
) ?? ) ??
false; false;
final isTextOnly = post.fileType == 'text' ||
(post.previewUrl.isEmpty &&
post.sampleUrl.isEmpty &&
post.fileUrl.isEmpty &&
!MediaUrlSelector.isVideo(post));
final shouldShowCloudCard = isTextOnly
? post.cloudLinks.isNotEmpty
: (post.cloudLinks.isNotEmpty ||
(post.commentary != null &&
post.commentary!.trim().isNotEmpty));
if (Responsive.isMobile(context)) { if (Responsive.isMobile(context)) {
if (currentIndex >= 0 && feedPosts.length > 1) { if (currentIndex >= 0 && feedPosts.length > 1) {
@@ -341,14 +351,12 @@ class PostDetailsScreen extends ConsumerWidget {
localMedia: localMedia, localMedia: localMedia,
fileSizeBytes: fileSizeBytes, fileSizeBytes: fileSizeBytes,
), ),
if (post.cloudLinks.isNotEmpty || if (shouldShowCloudCard) ...[
(post.commentary != null &&
post.commentary!.trim().isNotEmpty)) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
CloudMirrorsCard( CloudMirrorsCard(
links: post.cloudLinks, links: post.cloudLinks,
strings: strings, strings: strings,
commentary: post.commentary, commentary: isTextOnly ? null : post.commentary,
onPlayStream: (streamUrl) => launchUrl( onPlayStream: (streamUrl) => launchUrl(
Uri.parse(streamUrl), Uri.parse(streamUrl),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
@@ -438,6 +446,16 @@ class PostDetailsScreen extends ConsumerWidget {
t.status == DownloadTaskStatus.queued), t.status == DownloadTaskStatus.queued),
) ?? ) ??
false; false;
final isTextOnly = post.fileType == 'text' ||
(post.previewUrl.isEmpty &&
post.sampleUrl.isEmpty &&
post.fileUrl.isEmpty &&
!MediaUrlSelector.isVideo(post));
final shouldShowCloudCard = isTextOnly
? post.cloudLinks.isNotEmpty
: (post.cloudLinks.isNotEmpty ||
(post.commentary != null &&
post.commentary!.trim().isNotEmpty));
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
@@ -562,14 +580,12 @@ class PostDetailsScreen extends ConsumerWidget {
localMedia: localMedia, localMedia: localMedia,
fileSizeBytes: fileSizeBytes, fileSizeBytes: fileSizeBytes,
), ),
if (post.cloudLinks.isNotEmpty || if (shouldShowCloudCard) ...[
(post.commentary != null &&
post.commentary!.trim().isNotEmpty)) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
CloudMirrorsCard( CloudMirrorsCard(
links: post.cloudLinks, links: post.cloudLinks,
strings: strings, strings: strings,
commentary: post.commentary, commentary: isTextOnly ? null : post.commentary,
onPlayStream: (streamUrl) => launchUrl( onPlayStream: (streamUrl) => launchUrl(
Uri.parse(streamUrl), Uri.parse(streamUrl),
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
@@ -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,19 +180,75 @@ 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
? 'Зеркала (${links.length})'
: 'Mirrors (${links.length})',
), ),
const SizedBox(height: 10), ),
ButtonSegment(
value: 1,
icon: const Icon(Icons.notes_rounded, size: 16),
label: Text(
widget.strings.ru ? 'Описание' : 'Description',
),
),
],
selected: {_selectedTab},
onSelectionChanged: (set) =>
setState(() => _selectedTab = set.first),
),
),
),
IndexedStack(
index: _selectedTab,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: links
.map((link) => _buildLinkTile(
context,
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( Row(
children: [ children: [
Icon( Icon(
@@ -179,7 +258,7 @@ class CloudMirrorsCard extends StatelessWidget {
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
strings.ru ? 'Описание от автора' : 'Artist Commentary', widget.strings.ru ? 'Описание от автора' : 'Artist Commentary',
style: theme.textTheme.labelLarge?.copyWith( style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@@ -189,28 +268,20 @@ class CloudMirrorsCard extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FormattedContentText( FormattedContentText(
text: commentary!.trim(), text: text.trim(),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
height: 1.45, height: 1.45,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
Builder( if (creatorLinks.isNotEmpty) ...[
builder: (context) { const SizedBox(height: 10),
final creatorLinks = CreatorLink.extractLinks(commentary); CreatorLinkChips(
if (creatorLinks.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 10),
child: CreatorLinkChips(
links: creatorLinks, links: creatorLinks,
title: strings.ru ? 'Ссылки автора' : 'Author Links', title: widget.strings.ru ? 'Ссылки автора' : 'Author Links',
),
);
},
), ),
], ],
], ],
),
); );
} }
@@ -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
+168 -5
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,7 +250,8 @@ 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)
if (!isTextPost)
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@@ -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;
} }
@@ -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.0+74
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
+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);