Compare commits

..
28 Commits
Author SHA1 Message Date
RarDog d5916e01b5 fix(artists): resolve double keyboard inset and white square bug during search (v3.5.2)
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 13:16:00 +03:00
RarDog a535f2be39 fix(artists): refine Liquid Glass search bar, clean filter bar, and bump to v3.5.1
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 12:58:05 +03:00
RarDog 774e11f312 fix(linux): add C language and suppress incompatible pointer warning in CMake
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 12:47:58 +03:00
RarDog d7cbcc3f19 feat(artists): Liquid Glass redesign for Artists tab and bump version to v3.5.0
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 12:43:41 +03:00
RarDog 46f8b2dff3 chore(macos): update GeneratedPluginRegistrant with media_kit 2026-09-05 12:37:52 +03:00
RarDog 62b16d6deb fix(ci): set macOS deployment target 10.15 and use Flutter stable channel in CI 2026-09-05 12:27:55 +03:00
RarDog 70d5bad775 fix(ci): pin windows-2022, add media_kit_libs_macos_video and enable desktop in CI 2026-09-05 12:24:03 +03:00
RarDog dd5e5b5d65 ci: add macOS & Windows builds with artifact upload 2026-09-05 12:20:08 +03:00
RarDog 29d11cd848 docs: add MIT License 2026-09-05 12:05:05 +03:00
RarDog 7ec6d67738 chore: add *.zip to .gitignore 2026-09-05 11:50:01 +03:00
RarDog 1fb07b1247 feat(macos): add macOS desktop support with network entitlements 2026-09-05 11:38:45 +03:00
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
RarDog 15bebd6e47 fix: fullscreen play-pause button, search clear providers, video volume persistence & codec fallback (v3.1.6)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 23:09:23 +03:00
RarDog 295499ca4b feat: persist video volume, fix fullscreen controls, search providers & comments headroom (v3.1.5)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 22:50:15 +03:00
RarDog 9a0fd282b7 feat(video): optimize video player aspect ratio sizing and seekbar positioning v3.1.4
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 21:50:17 +03:00
RarDog 254f0e3d59 feat: Liquid Glass redesign for providers and network check, and tab back navigation (v3.1.3)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 21:29:25 +03:00
RarDog 5a568a3df9 perf: optimize post tags scrolling performance and eliminate frame drops (v3.1.2)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 21:11:03 +03:00
RarDog d58b512020 feat: colored tag categories division like e621/gelbooru/rule34 and fix search for tags with parentheses
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 20:56:35 +03:00
RarDog 384c07955c feat(ui): iPhone Liquid Glass redesign for Search, Favorites, and History, fix cheat sheet clearance and update sheet
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 19:06:37 +03:00
RarDog ec594595c9 fix: adjust settings button position and backup/json buttons layout (v3.0.9)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 18:42:00 +03:00
RarDog 586e3fc4db feat: iPhone-style liquid glass settings and floating button (v3.0.8)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 18:32:16 +03:00
103 changed files with 11163 additions and 4112 deletions
+59 -4
View File
@@ -12,7 +12,7 @@ permissions:
jobs:
build-windows:
name: Build Windows x64
runs-on: windows-latest
runs-on: windows-2022
steps:
- name: Checkout Code
@@ -21,10 +21,12 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.4'
channel: 'stable'
cache: true
- name: Enable Windows Desktop
run: flutter config --enable-windows-desktop
- name: Install Dependencies
run: flutter pub get
@@ -35,13 +37,66 @@ jobs:
shell: pwsh
run: |
$tag = "${{ github.ref_name }}"
if (-not $tag) { $tag = "v3.0.0" }
$zipName = "Lunaris-$tag-windows-x64.zip"
if (-not $tag) { $tag = "v3.4.1" }
$zipName = "Prisma-$tag-windows-x64.zip"
Compress-Archive -Path "build\windows\x64\runner\Release\*" -DestinationPath $zipName
echo "ZIP_NAME=$zipName" >> $env:GITHUB_ENV
- name: Upload Windows Artifact
uses: actions/upload-artifact@v4
with:
name: Prisma-windows-x64
path: ${{ env.ZIP_NAME }}
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: ${{ env.ZIP_NAME }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos:
name: Build macOS
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Enable macOS Desktop
run: flutter config --enable-macos-desktop
- name: Install Dependencies
run: flutter pub get
- name: Build macOS Release
run: flutter build macos --release
- name: Package macOS Zip
run: |
TAG="${{ github.ref_name }}"
if [ -z "$TAG" ]; then TAG="v3.4.1"; fi
ZIP_NAME="Prisma-$TAG-macos.zip"
cd build/macos/Build/Products/Release
zip -r -y "$GITHUB_WORKSPACE/$ZIP_NAME" Prisma.app
echo "ZIP_NAME=$ZIP_NAME" >> $GITHUB_ENV
- name: Upload macOS Artifact
uses: actions/upload-artifact@v4
with:
name: Prisma-macos
path: ${{ env.ZIP_NAME }}
- name: Upload to GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: ${{ env.ZIP_NAME }}
env:
+1
View File
@@ -12,3 +12,4 @@ android/local.properties
*.log
*.apk
*.tar.gz
*.zip
+6 -6
View File
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
revision: "d3b14c876900e553bc736ca19295fc09e3853e8e"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: linux
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
- platform: macos
create_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
base_revision: d3b14c876900e553bc736ca19295fc09e3853e8e
# User provided section
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 RarDog
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+11 -11
View File
@@ -1,7 +1,7 @@
# 🌙 Lunaris
# 💎 Prisma
<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 align="center">
@@ -10,16 +10,16 @@
</p>
<p align="center">
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.0.7-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.0.7" /></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/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" />
</p>
---
## 🚀 Что нового в Lunaris 3.0 (Major Release)
## 🚀 Что нового в Prisma 3.0 (Major Release)
### 🎨 Дизайн 2.0
- **Современный визуальный стиль**: Полный переход на squircle-карточки, адаптивную цветовую палитру и улучшенную Material You динамическую тему.
@@ -28,7 +28,7 @@
### 🐾 Глубокая интеграция Pawchive 2.0
- **Мульти-аккаунты**: Поддержка авторизации по логину и паролю, а также прямому вводу `Session Cookie`.
- **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Lunaris и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап.
- **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Prisma и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап.
- **Моментальная синхронизация**: При нажатии кнопки «В избранное» у любого автора (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/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
# Клонирование репозитория
git clone https://github.com/RarDog/Lunaris.git
cd Lunaris
git clone https://github.com/RarDog/Prisma.git
cd Prisma
# Установка зависимостей
flutter pub get
@@ -155,7 +155,7 @@ lib/
## 🔒 Безопасность и конфиденциальность
- **100% локальное хранение**: Вся история, закладки, локальные авторы и конфигурации хранятся только в вашей локальной базе на устройстве.
- **Никакой телеметрии**: Lunaris не собирает аналитику, не отправляет логи и уважает вашу приватность.
- **Никакой телеметрии**: Prisma не собирает аналитику, не отправляет логи и уважает вашу приватность.
- **Прямое соединение**: Запросы отправляются напрямую к выбранным API без прокси-серверов посредников.
---
+1
View File
@@ -7,6 +7,7 @@ analyzer:
- android/**
- windows/**
- linux/**
- macos/**
linter:
rules:
+1 -1
View File
@@ -3,7 +3,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:label="Lunaris"
android:label="Prisma"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
+4 -4
View File
@@ -1,7 +1,7 @@
# ❓ Часто задаваемые вопросы (FAQ)
### 1. Как обновить приложение?
Lunaris проверяет наличие свежих версий автоматически при запуске.
Prisma проверяет наличие свежих версий автоматически при запуске.
Вы также можете нажать **«Проверить обновления»** в Настройках.
Приложение определит архитектуру вашего Android-устройства и скачает оптимальный APK напрямую из репозитория.
@@ -13,13 +13,13 @@ Lunaris проверяет наличие свежих версий автома
---
### 3. Где сохраняются скачанные файлы?
- **На Android**: В стандартную папку `Pictures/Lunaris` или `Download/Lunaris` (в зависимости от настроек системы). Файлы сразу видны в стандартной Галерее телефона.
- **На Linux**: В директорию `~/Pictures/Lunaris` или папку загрузок.
- **На Android**: В стандартную папку `Pictures/Prisma` или `Download/Prisma` (в зависимости от настроек системы). Файлы сразу видны в стандартной Галерее телефона.
- **На Linux**: В директорию `~/Pictures/Prisma` или папку загрузок.
---
### 4. Безопасно ли вводить логин и пароль от Pawchive?
Абсолютно безопасно. Lunaris не отправляет ваши учетные данные никуда, кроме официального защищенного API Pawchive по HTTPS. Пароли не логируются и сохраняются в зашифрованном системном хранилище. Для дополнительного спокойствия вы можете использовать вход через **Session Cookie**.
Абсолютно безопасно. Prisma не отправляет ваши учетные данные никуда, кроме официального защищенного API Pawchive по HTTPS. Пароли не логируются и сохраняются в зашифрованном системном хранилище. Для дополнительного спокойствия вы можете использовать вход через **Session Cookie**.
---
+8 -8
View File
@@ -2,9 +2,9 @@
### 📥 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 | Для каких устройств |
|---|---|
@@ -13,27 +13,27 @@
| **`app-x86_64-release.apk`** | Эмуляторы Android на ПК (Bluestacks, Android Studio) или планшеты с процессорами Intel/AMD. |
> [!TIP]
> **Умное автообновление:** При проверке обновлений прямо внутри Lunaris приложение **самостоятельно определит архитектуру** вашего процессора и автоматически скачает правильный файл!
> **Умное автообновление:** При проверке обновлений прямо внутри Prisma приложение **самостоятельно определит архитектуру** вашего процессора и автоматически скачает правильный файл!
#### Шаги установки:
1. Скачайте нужный `.apk` файл на устройство.
2. Откройте файл через менеджер файлов и подтвердите установку («Разрешить установку из этого источника»).
3. Запустите **Lunaris**.
3. Запустите **Prisma**.
---
### 🐧 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. Распакуйте архив в любую удобную папку:
```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. Запустите приложение:
```bash
cd ~/Lunaris
cd ~/Prisma
./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, глубокую кастомизацию, приватность и удобную работу с медиа.
+2 -2
View File
@@ -1,6 +1,6 @@
# 🎬 Медиаплеер и просмотр
Экран просмотра в Lunaris оптимизирован для быстрого, плавного и естественного взаимодействия с контентом.
Экран просмотра в Prisma оптимизирован для быстрого, плавного и естественного взаимодействия с контентом.
---
@@ -17,7 +17,7 @@
## 📐 Определение разрешения медиа (1920×1080 и др.)
В Lunaris 3.0 внедрено **интеллектуальное ленивое определение разрешения**:
В Prisma 3.0 внедрено **интеллектуальное ленивое определение разрешения**:
- Для изображений и видео в инфо-панели поста отображаются точные физические размеры (например, `1920 × 1080`, `3840 × 2160`).
- Определение происходит динамически через заголовки и потоковый декодер — приложению не требуется скачивать многомегабайтный файл целиком только ради того, чтобы узнать его габариты.
+2 -2
View File
@@ -1,6 +1,6 @@
# 💾 Офлайн-режим и загрузки
Lunaris предоставляет полноценный оффлайн-хаб для работы с вашей личной коллекцией без подключения к интернету.
Prisma предоставляет полноценный оффлайн-хаб для работы с вашей личной коллекцией без подключения к интернету.
---
@@ -26,7 +26,7 @@ Lunaris предоставляет полноценный оффлайн-хаб
*Пример шаблона:*
```text
Lunaris/{Provider}/{Artist}/{ID}.{ext}
Prisma/{Provider}/{Artist}/{ID}.{ext}
```
Файлы будут аккуратно раскладываться по папкам авторов и провайдеров, сохраняя порядок в вашей галерее.
+1 -1
View File
@@ -1,6 +1,6 @@
# 🐾 Интеграция и синхронизация с 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)
- `dlsite` (DLsite)
- `discord` (Серверы Discord)
- **Возможности в Lunaris:**
- **Возможности в Prisma:**
- Просмотр глобальной ленты свежих постов.
- Персональные карточки авторов с аватаром, баннером, анонсами и ссылками на оригинальные профили.
- Полноценная авторизация в профиль 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`) возвращают только посты, где **присутствуют оба тега одновременно**.
Lunaris поддерживает специальный оператор **`and`**:
Prisma поддерживает специальный оператор **`and`**:
```text
genshin_impact and honkai_star_rail
```
+3 -3
View File
@@ -1,4 +1,4 @@
### 🌙 Lunaris Wiki
### 🌙 Prisma Wiki
- **[Главная](Home)**
- **[Быстрый старт и установка](Getting-Started)**
@@ -12,5 +12,5 @@
---
**[Скачать Lunaris](https://github.com/RarDog/Lunaris/releases)**
**[Репозиторий GitHub](https://github.com/RarDog/Lunaris)**
**[Скачать Prisma](https://github.com/RarDog/Prisma/releases)**
**[Репозиторий GitHub](https://github.com/RarDog/Prisma)**
@@ -1,4 +1,4 @@
#define MyAppName "Lunaris"
#define MyAppName "Prisma"
#define MyAppVersion "2.0.1"
#define MyAppPublisher "RarDog"
#define MyAppExeName "gel_rule_app.exe"
@@ -13,7 +13,7 @@ DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
PrivilegesRequired=lowest
OutputDir=..\dist
OutputBaseFilename=LunarisSetup
OutputBaseFilename=PrismaSetup
SetupIconFile=..\windows\runner\resources\app_icon.ico
Compression=lzma2
SolidCompression=yes
+2 -2
View File
@@ -40,7 +40,7 @@ class GelRuleApp extends ConsumerWidget {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp.router(
title: 'Lunaris',
title: 'Prisma',
debugShowCheckedModeBanner: false,
theme: buildTheme(
Brightness.light,
@@ -97,7 +97,7 @@ class _AppOverlayState extends ConsumerState<_AppOverlay> {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text('${strings.appUpdateAvailable}: Lunaris ${info.version}'),
title: Text('${strings.appUpdateAvailable}: Prisma ${info.version}'),
content: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: SingleChildScrollView(
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.0.7';
const appBuildNumber = 58;
const appDisplayVersion = '3.5.2';
const appBuildNumber = 78;
const appReleaseChannel = 'Stable';
+242 -43
View File
@@ -1,5 +1,201 @@
const lunarisChangelog = [
LunarisChange(
const prismaChangelog = [
PrismaChange(
version: '3.5.2',
title: 'Исправление наложения клавиатуры и поиска авторов',
bullets: [
'Устранена ошибка выдвижения белого прямоугольника: исправлен конфликт двойного учета клавиатурного отступа (double viewInsets) во вложенных скаффолдах.',
'Корректная видимость авторов: карточки артистов больше не сжимаются и остаются полностью видимыми и прокручиваемыми при вводе запроса.',
'Улучшенное поведение интерфейса: плавающая кнопка настроек и нижняя панель скрываются при открытии клавиатуры, добавлен свайп для скрытия клавиатуры при прокрутке.',
],
),
PrismaChange(
version: '3.5.1',
title: 'Улучшение панели поиска и фильтров авторов в Liquid Glass',
bullets: [
'Чистый дизайн поиска: убрана лишняя заливка поля ввода и тяжелые рамки, поиск авторов стал идеально прозрачным и монолитным.',
'Единая компактная строка фильтров: провайдеры и платформы объединены в элегантную полосу прокрутки без нагромождения строк.',
'Нейтральные бейджи и аккуратные даты: скорректированы цвета счетчиков постов и укорочен вывод времени обновления.',
],
),
PrismaChange(
version: '3.5.0',
title: 'Редизайн вкладки «Артисты» в стиле Liquid Glass',
bullets: [
'Дизайн-код Liquid Glass: карточки авторов и панель поиска переведены на эстетику матового стекла с эффектом размытия BackdropFilter и глянцевыми окантовками.',
'Цветовое кодирование сервисов: для платформ Patreon, Fanbox, Fantia, Boosty и Discord добавлены фирменные неоновые фильтры и светящиеся бейджи.',
'Интерактивные карточки: squircle-аватары с деликатным свечением, анимированная кнопка избранного с эффектом золотого сияния.',
'Стеклянное модальное окно избранного: обновленный диалог любимых авторов с полупрозрачным блюром и стильной панелью Pawchive-синхронизации.',
],
),
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',
title: 'Надежный полноэкранный плеер, сохранение провайдеров в поиске, громкость и устранение ошибки кодека',
bullets: [
'Полноэкранный плеер — отзывчивая кнопка Пуск/Пауза: центральная кнопка управления воспроизведением полностью переработана на непроницаемый GestureDetector со сквиркл-откликом, гарантируя мгновенное срабатывание без перехвата внешними жестами.',
'Сохранение источников при нажатии на крестик в поиске: клик по кнопке сброса в строке поиска теперь очищает только поисковый запрос и теги, никогда не сбрасывая выбранные источники до «Все».',
'Безупречное сохранение громкости: громкость видеоплеера корректно инициализируется из настроек, сохраняется при регулировках жестом и переходе между постами, не сбрасываясь на 100% при открытии нового видео и перезапуске приложения.',
'Автоматический fallback для ошибки Could not open codec: видеоплеер получил безопасные профили декодирования и автопереключение на программный лавк-декодер (FFmpeg software fallback) при редких сбоях аппаратного ускорения.',
],
),
PrismaChange(
version: '3.1.5',
title: 'Сохранение громкости видео, исправление полноэкранного плеера, фильтрации и удобства комментариев',
bullets: [
'Сохранение уровня громкости (Persistent Volume): выбранная громкость видеоплеера теперь сохраняется при свайп-регулировке и переключениях между видео и сохраняется даже после перезапуска приложения.',
'Исправление кнопок в полноэкранном режиме: кнопки паузы/воспроизведения и быстрой перемотки на ±10 секунд в центре экрана теперь работают мгновенно и надежно с анимацией визуального отклика (Seek Ripple).',
'Сохранение выбранных источников при очистке тегов: удаление всех тегов в строке поиска больше не сбрасывает выбранные провайдеры до значения «Все», сохраняя ваши активные источники.',
'Плавная строка поиска: при выборе тега из подсказок поле ввода больше не сдвигается слишком далеко в сторону, оставляя тег и курсор в поле зрения.',
'Удобный доступ к комментариям в посте: увеличен отступ снизу в деталях поста и расширена зона клика блока комментариев, позволяя комфортно прокрутить их выше системных жестов навигации.',
],
),
PrismaChange(
version: '3.1.4',
title: 'Оптимизация видеоплеера — адаптивная высота и идеальное прилегание полосы воспроизведения',
bullets: [
'Устранение зазора под видео: контейнер плеера на мобильных устройствах и ПК теперь динамически подстраивается под пропорции видео (aspect ratio) вместо жестко фиксированной высоты 62% экрана, устраняя огромные пустые черные полосы сверху и снизу.',
'Точное позиционирование полосы воспроизведения: исправлен отступ снизу и ложное наложение системных insets на неполноэкранный плеер в карточке поста — ползунок воспроизведения, таймкоды и кнопка фуллскрина теперь аккуратно прилегают к нижнему краю видео без зависания в воздухе.',
'Компактные элементы управления: выровнены буферный трек и слайдер прокрутки, оптимизирован градиент затенения для максимальной видимости и удобства управления.',
],
),
PrismaChange(
version: '3.1.3',
title: 'iPhone Liquid Glass — Источники, Проверка сети и Навигация назад по вкладкам',
bullets: [
'Возврат назад по вкладкам (Tab Back Navigation): реализован стек истории переходов между разделами в AppShell — теперь при нажатии кнопки «Назад» или системного жеста происходит возврат к ранее открытым вкладкам и подразделам вместо случайного закрытия приложения.',
'Liquid Glass редизайн списка источников (Providers): стеклянные карточки провайдеров с красивыми сквиркл-иконками движков (Gelbooru, Rule34, Danbooru, e621, Yande.re, Paheal, Kemono), переключателями активности, инфо-бейджами приоритета и зеркальными кнопками управления.',
'Liquid Glass редизайн редактирования и создания источников (Edit / New Provider): секционные стеклянные карточки с градиентными бейджами, полоса быстрых пресетов для мгновенного заполнения URL и типа API, безопасный ввод API-ключей и наглядные подсказки.',
'Liquid Glass редизайн проверки состояния источников (Provider Check): карточка общих метрик сети (онлайн/офлайн, средний пинг), раскрывающиеся аккордеоны детальной диагностики, живые светодиодные LED-индикаторы статуса и кнопка одновременной проверки всех провайдеров.',
],
),
PrismaChange(
version: '3.1.2',
title: 'Оптимизация производительности: устранение лагов при скролле к тегам',
bullets: [
'Устранение лагов при прокрутке тегов: полностью ликвидирован тяжелый GPU-оверхед BackdropFilter на повторяющихся чипах тегов, вызывавший просадки FPS и микрофризы при скролле постов с десятками и сотнями тегов.',
'Высокопроизводительный стеклянный стиль чипов (TagChip): чипы переведены на эффективные аппаратные альфа-слои и зеркальные контуры без потери премиального неонового Liquid Glass вида и цветных индикаторов категорий.',
'Облегчение контейнеров блока тегов: на мобильных и десктоп экранах контейнеры тегов оптимизированы для мгновенного плавного 120 FPS скроллинга.',
'Интеллектуальный лимит начального отображения: оптимизировано первоначальное количество отображаемых тегов в огромных категориях с удобной кнопкой быстрого раскрытия.',
],
),
PrismaChange(
version: '3.1.1',
title: 'Деление тегов по стилю e621/Gelbooru/Rule34 и исправление поиска тегов со скобками',
bullets: [
'Цветное деление тегов по категориям (e621, Rule34, Gelbooru): теги авторов (Artist) подсвечиваются кораллово-красным, персонажи (Character) — мятно-зелёным, серии/копирайты (Copyright) — пурпурным, виды (Species) — янтарно-оранжевым, мета (Meta) — ледяным голубым, а общие теги (General) — нейтральным стилем.',
'Liquid Glass чипы тегов: компоненты TagChip получили полупрозрачный фон с BackdropFilter размытием, цветной светодиодный индикатор-точку со свечением и зеркальный контур.',
'Индикаторы в заголовках категорий: заголовки блоков тегов дополнены цветными точками категорий со свечением и стильными капсулами счётчиков.',
'Исправление поиска тегов со скобками: исправлена проблема, из-за которой теги с круглыми скобками (например, tyson_(password), touhou_(series)) очищались от скобок при поиске и не находили постов в ленте.',
'Поддержка скобок в подсказках и локальном кэше: автодополнение теперь корректно выдает теги со скобками, фильтруя только внешние скобки группировки and-веток.',
'Liquid Glass оформление блока тегов в детальном просмотре поста на мобильных и десктоп устройствах.',
],
),
PrismaChange(
version: '3.1.0',
title: 'iPhone Liquid Glass — Поиск, Избранное, История и исправление доступности',
bullets: [
'Liquid Glass редизайн разделов «Поиск», «Избранное» и «История»: матовые стеклянные карточки (BackdropFilter blur), полупрозрачные градиенты, зеркальные контуры и сквиркл-бейджи иконок в стиле iOS.',
'Исправление шпаргалки по поиску: увеличен нижний отступ прокрутки во всех списках и сетках до 140px + safe area, благодаря чему блок операторов и шпаргалка по поиску больше не перекрываются парящей навигационной панелью и легко нажимаются.',
'Исправление модального окна проверки обновлений: панель с выбором источников (GitHub и Gitea) открывается через корневой навигатор поверх всей навигации с безопасным отступом снизу, обеспечивая беспрепятственный доступ к обеим кнопкам.',
'Тактильный отклик и стеклянные пилюли: быстрые операторы, категории, недавние запросы, фильтры медиа и переключатели разделов получили матовое оформление и вибрационный отклик Haptic Feedback.',
],
),
PrismaChange(
version: '3.0.9',
title: 'Коррекция позиции кнопки настроек и отображения кнопок JSON и бэкапа',
bullets: [
'Оптимальная позиция кнопки настроек: парящая стеклянная кнопка опущена прямо к нижней навигационной панели, без избыточного завышения над карточками контента.',
'Полная видимость кнопок JSON и бэкапа: добавлен увеличенный нижний отступ прокрутки в настройках, чтобы панель навигации не перекрывала кнопки при скролле до конца.',
'Улучшенный дизайн и сетка кнопок экспорта/импорта: кнопки бэкапа и JSON организованы в симметричные аккуратные строки с яркими акцентными бейджами.',
],
),
PrismaChange(
version: '3.0.8',
title: 'iPhone Liquid Glass — раздел «Настройки» и парящая стеклянная кнопка',
bullets: [
'Парящая стеклянная кнопка настроек (Liquid Glass Floating Button): круглая сфера из матового стекла с эффектом BackdropFilter blur, зеркальным радиальным градиентом, подсветкой ободка, пружинящей анимацией масштабирования и вращения при нажатии.',
'Карточки настроек в стиле Liquid Glass (1:1 iOS): группы настроек переработаны в матовые стеклянные карточки с мягким градиентом, зеркальной рамкой и многослойными тенями глубины.',
'Иконки разделов и пунктов в стиле iOS: яркие сквиркл-бейджи с акцентными градиентами, мягким свечением и четкими белыми иконками, как в нативных настройках iPhone.',
'Стеклянная быстрая навигация по категориям: зафиксированная панель категорий с размытием фона и тактильным откликом при переключении разделов.',
],
),
PrismaChange(
version: '3.0.7',
title: 'Liquid Glass Navigation Bar — парящая стеклянная панель и анимированная пилюля',
bullets: [
@@ -9,7 +205,7 @@ const lunarisChangelog = [
'Глубина контента: контент ленты, поиска и галереи плавно просвечивает и размывается под стеклянной панелью при прокрутке.',
],
),
LunarisChange(
PrismaChange(
version: '3.0.6',
title: 'Улучшение недавних запросов и отображения тегов',
bullets: [
@@ -19,7 +215,7 @@ const lunarisChangelog = [
'Удобное удаление: увеличенная область нажатия для кнопки удаления (крестика) предотвращает случайные нажатия на сам запрос.',
],
),
LunarisChange(
PrismaChange(
version: '3.0.5',
title: 'Интерактивные ссылки на соцсети и авторов в объявлениях и описаниях',
bullets: [
@@ -28,7 +224,7 @@ const lunarisChangelog = [
'Корректная очистка пунктуации: знаки препинания в конце URL (!, ?, :, ., ,, ;, >) больше не ломают ссылки.',
],
),
LunarisChange(
PrismaChange(
version: '3.0.0',
title: 'Lunaris 3.0 — Глобальное обновление: Дизайн 2.0, Интеграция Pawchive и умная синхронизация',
bullets: [
@@ -41,7 +237,7 @@ const lunarisChangelog = [
'Исправление вёрстки: устранены переносы текста в заголовке окна аккаунтов и карточках пользователей на экранах смартфонов.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.10',
title: 'Выгрузка локального избранного в Pawchive и двусторонняя синхронизация',
bullets: [
@@ -51,7 +247,7 @@ const lunarisChangelog = [
'Синхронизация в реальном времени с любых провайдеров: при добавлении автора в избранное в лентах Kemono, Coomer или Pawchive изменение мгновенно передаётся на сервер Pawchive.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.9',
title: 'Исправление входа и синхронизации аккаунтов Pawchive',
bullets: [
@@ -60,7 +256,7 @@ const lunarisChangelog = [
'Информативные ошибки: понятные сообщения на русском при неверном пароле или сбое сети вместо сырых исключений Dio.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.8',
title: 'ABI-Aware Auto-Update — правильный APK для вашего процессора',
bullets: [
@@ -70,7 +266,7 @@ const lunarisChangelog = [
'Кеш ABI: архитектура определяется один раз за сеанс.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.7',
title: 'Lazy Media Dimensions for Pawchive & Unknown-Size Providers',
bullets: [
@@ -79,7 +275,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.6',
title: 'Pawchive Accounts Integration & Favorites Synchronization',
bullets: [
@@ -90,7 +286,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.5',
title: 'Fullscreen Video Auto-Rotation & Orientation Controls',
bullets: [
@@ -99,7 +295,7 @@ const lunarisChangelog = [
'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',
title: 'OverlayPortal Architecture & Search UI Unblocking',
bullets: [
@@ -110,7 +306,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.3',
title: 'Tag Suggestions Overlay Fix & Rule34 API Alignment',
bullets: [
@@ -120,7 +316,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.2',
title: 'Gelbooru DAPI Compliance & Tag Query Optimization',
bullets: [
@@ -130,7 +326,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.1',
title: 'Cloud Mirrors & Author Announcements Fixes',
bullets: [
@@ -139,7 +335,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.3.0',
title: 'Artist Cloud Drives & External Video Mirrors',
bullets: [
@@ -151,7 +347,7 @@ const lunarisChangelog = [
'Artist Commentary Card: formatted display of author notes, instructions, and text below post details.',
],
),
LunarisChange(
PrismaChange(
version: '2.2.0',
title: 'Offline Mode Overhaul & Post Details Screen Redesign',
bullets: [
@@ -162,7 +358,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.1.0',
title: '4-Tab Major Redesign: Search, Favorites, History & Collections',
bullets: [
@@ -172,7 +368,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.15',
title: 'Settings 2.0 Layout Fixes & Pinned Quick Navigation',
bullets: [
@@ -183,7 +379,7 @@ const lunarisChangelog = [
'Header text overflow protections: wrapped title texts in Expanded across all settings cards.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.14',
title: 'Settings 2.0: Modern Grouped Design & Quick Ergonomics',
bullets: [
@@ -195,7 +391,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.13',
title: 'Toolbar Section Dividers & Post Card Badge Stacking Fix',
bullets: [
@@ -205,7 +401,7 @@ const lunarisChangelog = [
'Expanded Bottom Gradient: increased bottom cinematic gradient height to 64dp for clean text and badge contrast.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.12',
title: 'Feed 2.0: Pinterest Modern Redesign & Sleek Cards',
bullets: [
@@ -218,7 +414,7 @@ const lunarisChangelog = [
'Polished Search Presets: stylish bookmark capsules for quick access to saved search queries.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.11',
title: 'Mobile Video Player Redesign & Format Recognition Fix',
bullets: [
@@ -232,7 +428,7 @@ const lunarisChangelog = [
'Sleek Neon Seekbar with live network buffer progress indicator.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.10',
title: 'Fix "and" Tag Operator Retention, Suggestion Preceding Drafts & AND Chip Styling',
bullets: [
@@ -242,7 +438,7 @@ const lunarisChangelog = [
'Distinct visual styling for AND separator chips with secondary theme container and alt-route icon.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.9',
title: 'Instant Tag Suggestions, Persistent Disk Cache, Expanded History & Match Highlighting',
bullets: [
@@ -256,7 +452,7 @@ const lunarisChangelog = [
'Dedicated Clear Tag Cache actions in Settings and Cache Manager.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.8',
title: 'Shared Context Modifiers for Multi-Tag Search & Clean Tag Filtering',
bullets: [
@@ -265,7 +461,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.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.7',
title: 'Independent Multi-Tag Search Streams, Local "and" Connector, Pawchive in Main Feed & Reverts',
bullets: [
@@ -276,7 +472,7 @@ const lunarisChangelog = [
'Removed external linked accounts bar from artist posts screen.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.6',
title: 'Pawchive Deep Integration: Global Posts Feed, Artist Tags, Announcements, Links & Server Media Filter',
bullets: [
@@ -287,7 +483,7 @@ const lunarisChangelog = [
'Server-side media filtering (Photos, Videos, GIFs) querying the entire author archive through the API.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.5',
title: '10 QoL Super-Update: AMOLED, Dynamic Colors, Grid switch, Share, Presets, Cache & Backup',
bullets: [
@@ -302,7 +498,7 @@ const lunarisChangelog = [
'Full JSON backup export and import for seamless configuration sync.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.4',
title: 'Platform service selection & media type filtering in artist posts',
bullets: [
@@ -311,7 +507,7 @@ const lunarisChangelog = [
'Full-screen post pager respects active media type filters during swiping.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.3',
title: 'Infinite artist pagination, favorite artists modal, and card layout polish',
bullets: [
@@ -321,7 +517,7 @@ const lunarisChangelog = [
'Removed mapping truncation in Pawchive provider to deliver all post attachments cleanly.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.2',
title: 'Swipeable media pager, favorite artists, performance and Pawchive',
bullets: [
@@ -333,7 +529,7 @@ const lunarisChangelog = [
'In-app update checks and downloads connected directly to RarDog GitHub releases.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.1',
title: 'Localization, zoom and favorite downloads',
bullets: [
@@ -344,7 +540,7 @@ const lunarisChangelog = [
'Favorites can optionally auto-download media in the background, with local files tracked separately from favorites.',
],
),
LunarisChange(
PrismaChange(
version: '2.0.0',
title: 'Customization and provider polish',
bullets: [
@@ -355,7 +551,7 @@ const lunarisChangelog = [
'Kemono and Coomer creator search uses the current mbahArip keyword endpoints with public API fallbacks.',
],
),
LunarisChange(
PrismaChange(
version: '1.1.2',
title: 'Experimental tag and Artists update',
bullets: [
@@ -365,7 +561,7 @@ const lunarisChangelog = [
'This is an experimental release for checking the new tag and Artists behavior.',
],
),
LunarisChange(
PrismaChange(
version: '1.1.1',
title: 'Artists and in-app updates',
bullets: [
@@ -375,7 +571,7 @@ const lunarisChangelog = [
'Tag input keeps its scroll position steadier while typing and committing chips.',
],
),
LunarisChange(
PrismaChange(
version: '1.1.0',
title: 'Lunaris rebrand and providers',
bullets: [
@@ -385,7 +581,7 @@ const lunarisChangelog = [
'Video player UI preferences are saved across fullscreen, details and restarts.',
],
),
LunarisChange(
PrismaChange(
version: '1.0.2',
title: 'Provider safety fix',
bullets: [
@@ -394,7 +590,7 @@ const lunarisChangelog = [
'Existing enabled legacy configs are disabled so the main feed is not polluted by known-offline APIs.',
],
),
LunarisChange(
PrismaChange(
version: '1.0.1',
title: 'Mobile polish',
bullets: [
@@ -403,7 +599,7 @@ const lunarisChangelog = [
'Tag chip input no longer jumps while typing.',
],
),
LunarisChange(
PrismaChange(
version: '1.0.0',
title: 'Stable 1.0',
bullets: [
@@ -413,7 +609,7 @@ const lunarisChangelog = [
'Windows portable package is published alongside APK and installer.',
],
),
LunarisChange(
PrismaChange(
version: '0.4.3',
title: 'Bugfixes',
bullets: [
@@ -425,8 +621,8 @@ const lunarisChangelog = [
),
];
class LunarisChange {
const LunarisChange({
class PrismaChange {
const PrismaChange({
required this.version,
required this.title,
required this.bullets,
@@ -436,3 +632,6 @@ class LunarisChange {
final String title;
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';
class AppBreakpoints {
@@ -6,11 +8,19 @@ class AppBreakpoints {
}
class Responsive {
static bool isMobile(BuildContext context) =>
MediaQuery.sizeOf(context).width < AppBreakpoints.mobile;
static bool isMobile(BuildContext context) {
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide < 600;
}
return MediaQuery.sizeOf(context).width < AppBreakpoints.mobile;
}
static bool isDesktop(BuildContext context) =>
MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop;
static bool isDesktop(BuildContext context) {
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
return MediaQuery.sizeOf(context).shortestSide >= 600;
}
return MediaQuery.sizeOf(context).width >= AppBreakpoints.desktop;
}
static int columnsFor(
BuildContext context, {
+1 -1
View File
@@ -224,7 +224,7 @@ final updateServiceProvider = Provider<UpdateService>((ref) {
connectTimeout: const Duration(seconds: 12),
receiveTimeout: const Duration(seconds: 12),
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',
},
),
+11 -1
View File
@@ -60,11 +60,21 @@ class Post {
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 {
return tags.where((tag) {
final t = tag.trim();
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.contains('"url"') || t.contains('"service"')) 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) {
final host = Uri.tryParse(baseUrl)?.host;
return {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*',
if (host != null && host.isNotEmpty) 'Referer': '$baseUrl/',
};
+34 -8
View File
@@ -30,7 +30,7 @@ class KemonoProvider
receiveTimeout: const Duration(seconds: 20),
headers: const {
'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
Future<List<ArtistAnnouncement>> getArtistAnnouncements(
String service, String artistId) async =>
const [];
String service, String artistId) async {
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';
@@ -385,17 +400,23 @@ class KemonoProvider
}
// 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 firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final isTextOnly = cloudLinks.isEmpty;
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror',
if (!isTextOnly) 'cloud_mirror' else 'text_post',
];
return [
Post(
@@ -411,14 +432,19 @@ class KemonoProvider
height: 0,
source: postSource,
createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link',
fileType: firstStreamable != null
? 'video'
: (isTextOnly ? 'text' : 'link'),
score: 0,
tagGroups: {
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
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),
headers: const {
'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.)
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 firstStreamable =
cloudLinks.where((l) => l.isStreamable).firstOrNull;
final firstLink = cloudLinks.isNotEmpty ? cloudLinks.first : null;
final fileUrl = firstStreamable?.directStreamUrl ?? firstLink?.url ?? '';
final isTextOnly = cloudLinks.isEmpty;
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
'cloud_mirror',
if (!isTextOnly) 'cloud_mirror' else 'text_post',
];
return [
Post(
@@ -444,14 +450,19 @@ class PawchiveProvider
height: 0,
source: postSource,
createdAt: published,
fileType: firstStreamable != null ? 'video' : 'link',
fileType: firstStreamable != null
? 'video'
: (isTextOnly ? 'text' : 'link'),
score: 0,
tagGroups: {
'artist': [query.artistName],
'meta': [query.service],
if (title.trim().isNotEmpty) 'copyright': [title.trim()],
if (cloudLinksJson.isNotEmpty) 'cloud_links': cloudLinksJson,
if (cleanDesc.isNotEmpty) 'description': [cleanDesc],
if (cleanDesc.isNotEmpty)
'description': [cleanDesc]
else if (rawContent.trim().isNotEmpty)
'description': [rawContent.trim()],
},
),
];
+11 -2
View File
@@ -169,7 +169,16 @@ class ProviderManager {
for (final raw in rawTags) {
var token = raw.trim();
token = token.replaceAll(RegExp(r'^[\(\)]+|[\(\)]+$'), '').trim();
if (token.isEmpty) continue;
// Only strip parentheses if they are grouping delimiters (unbalanced in the token, e.g. '(cat' or 'dog)')
// but preserve tags that have internal paired parentheses like 'tyson_(password)'.
if (token.startsWith('(') && !token.contains(')')) {
token = token.substring(1).trim();
}
if (token.endsWith(')') && !token.contains('(')) {
token = token.substring(0, token.length - 1).trim();
}
if (token.isEmpty) continue;
if (token.toLowerCase() == 'and') {
@@ -560,7 +569,7 @@ class ProviderManager {
);
}
return const Success({
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*',
});
}
+1 -1
View File
@@ -21,7 +21,7 @@ class Rule34Provider extends GelbooruProvider {
@override
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': '*/*',
'Referer': 'https://rule34.xxx/',
};
@@ -60,7 +60,7 @@ class ProviderRepository {
priority: 3,
timeoutSeconds: 20,
customHeaders: const {
'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser',
'User-Agent': 'Prisma/2.0.2 Flutter local booru browser',
},
createdAt: now,
updatedAt: now,
@@ -74,7 +74,7 @@ class ProviderRepository {
priority: 4,
timeoutSeconds: 20,
customHeaders: const {
'User-Agent': 'Lunaris/2.0.2 Flutter local booru browser',
'User-Agent': 'Prisma/2.0.2 Flutter local booru browser',
},
createdAt: now,
updatedAt: now,
+2 -2
View File
@@ -52,13 +52,13 @@ class BackupService {
Future<bool> exportBackup() async {
final json = await createBackupJson();
final fileName =
'lunaris_backup_${DateTime.now().millisecondsSinceEpoch}.json';
'prisma_backup_${DateTime.now().millisecondsSinceEpoch}.json';
if (Platform.isAndroid) {
final tempDir = await getTemporaryDirectory();
final file = File('${tempDir.path}/$fileName');
await file.writeAsString(json);
await Share.shareXFiles([XFile(file.path)], text: 'Lunaris Backup');
await Share.shareXFiles([XFile(file.path)], text: 'Prisma Backup');
return true;
}
+1 -1
View File
@@ -224,7 +224,7 @@ class DownloadService {
return {
'User-Agent': lower.contains('realbooru') || lower.contains('paheal')
? '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')
? 'video/webm,video/mp4,image/avif,image/webp,image/apng,image/*,*/*;q=0.8'
: '*/*',
@@ -94,7 +94,7 @@ class PawchiveSyncService {
followRedirects: false,
validateStatus: (status) => status != null && status < 500,
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',
},
),
@@ -231,7 +231,7 @@ class PawchiveSyncService {
headers: {
'Cookie': 'session=$cleanCookie',
'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,
),
@@ -509,7 +509,7 @@ class PawchiveSyncService {
final options = Options(
headers: {
'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) =>
status != null && (status == 200 || status == 201 || status == 204),
+1 -4
View File
@@ -21,10 +21,7 @@ class SearchService {
TagCacheService? get tagCacheService => _tagCacheService;
static String sanitizeToken(String raw) {
return raw
.replaceAll(RegExp(r'^[\(\)]+|[\(\)]+$'), '')
.trim()
.toLowerCase();
return raw.trim().toLowerCase();
}
List<String> parseTags(String query) {
+20 -1
View File
@@ -39,6 +39,7 @@ class AppSettings {
required this.motionRefreshMode,
required this.autoBatterySaver60Hz,
required this.videoPlayerMuted,
this.videoPlayerVolume = 100.0,
required this.videoPlayerHalfVolume,
required this.videoPlayerLoop,
required this.videoPlayerCover,
@@ -94,6 +95,7 @@ class AppSettings {
final String motionRefreshMode;
final bool autoBatterySaver60Hz;
final bool videoPlayerMuted;
final double videoPlayerVolume;
final bool videoPlayerHalfVolume;
final bool videoPlayerLoop;
final bool videoPlayerCover;
@@ -146,6 +148,7 @@ class AppSettings {
motionRefreshMode: 'auto',
autoBatterySaver60Hz: true,
videoPlayerMuted: false,
videoPlayerVolume: 100.0,
videoPlayerHalfVolume: false,
videoPlayerLoop: false,
videoPlayerCover: false,
@@ -191,6 +194,7 @@ class AppSettings {
String? motionRefreshMode,
bool? autoBatterySaver60Hz,
bool? videoPlayerMuted,
double? videoPlayerVolume,
bool? videoPlayerHalfVolume,
bool? videoPlayerLoop,
bool? videoPlayerCover,
@@ -252,6 +256,7 @@ class AppSettings {
motionRefreshMode: motionRefreshMode ?? this.motionRefreshMode,
autoBatterySaver60Hz: autoBatterySaver60Hz ?? this.autoBatterySaver60Hz,
videoPlayerMuted: videoPlayerMuted ?? this.videoPlayerMuted,
videoPlayerVolume: videoPlayerVolume ?? this.videoPlayerVolume,
videoPlayerHalfVolume:
videoPlayerHalfVolume ?? this.videoPlayerHalfVolume,
videoPlayerLoop: videoPlayerLoop ?? this.videoPlayerLoop,
@@ -309,6 +314,7 @@ class AppSettings {
'motionRefreshMode': motionRefreshMode,
'autoBatterySaver60Hz': autoBatterySaver60Hz,
'videoPlayerMuted': videoPlayerMuted,
'videoPlayerVolume': videoPlayerVolume,
'videoPlayerHalfVolume': videoPlayerHalfVolume,
'videoPlayerLoop': videoPlayerLoop,
'videoPlayerCover': videoPlayerCover,
@@ -392,6 +398,8 @@ class AppSettings {
defaults.autoBatterySaver60Hz,
videoPlayerMuted:
(json['videoPlayerMuted'] as bool?) ?? defaults.videoPlayerMuted,
videoPlayerVolume: (json['videoPlayerVolume'] as num?)?.toDouble() ??
defaults.videoPlayerVolume,
videoPlayerHalfVolume: (json['videoPlayerHalfVolume'] as bool?) ??
defaults.videoPlayerHalfVolume,
videoPlayerLoop:
@@ -601,6 +609,17 @@ class SettingsService {
return updateSettings(settings.copyWith(videoPlaybackPositions: positions));
}
Future<Result<void>> setVideoPlayerVolume(double volume) async {
final result = await getSettings();
if (result is Error<AppSettings>) return Error(result.failure);
final settings = (result as Success<AppSettings>).data;
final clamped = volume.clamp(0.0, 100.0);
if ((settings.videoPlayerVolume - clamped).abs() < 0.01) {
return const Success(null);
}
return updateSettings(settings.copyWith(videoPlayerVolume: clamped));
}
Future<Result<void>> appendDiagnosticLog(String message) async {
final result = await getSettings();
if (result is Error<AppSettings>) return Error(result.failure);
@@ -655,7 +674,7 @@ class SettingsService {
return Success(
const JsonEncoder.withIndent(' ').convert({
'app': {
'name': 'Lunaris',
'name': 'Prisma',
'version': appVersion,
'build': buildNumber,
'generatedAt': DateTime.now().toIso8601String(),
+13 -5
View File
@@ -25,7 +25,14 @@ class TagCacheService {
return _resolvedPath!;
}
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!;
}
@@ -66,10 +73,11 @@ class TagCacheService {
int limit = 16,
Iterable<String>? priorityTags,
}) {
final cleanPrefix = prefix
.replaceAll(RegExp(r'^[\(\)]+|[\(\)]+$'), '')
.trim()
.toLowerCase();
var cleanPrefix = prefix.trim().toLowerCase();
// If the prefix has an unbalanced opening parenthesis at the start (e.g. '(cat'), strip it
if (cleanPrefix.startsWith('(') && !cleanPrefix.contains(')')) {
cleanPrefix = cleanPrefix.substring(1).trim();
}
if (cleanPrefix.isEmpty) return const [];
final prioritySet = priorityTags
+9 -9
View File
@@ -18,14 +18,14 @@ class UpdateService {
UpdateService(this._dio, this._settingsService);
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 =
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases';
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Prisma/releases';
static const githubLatestReleaseUrl =
'https://api.github.com/repos/RarDog/Lunaris/releases/latest';
'https://api.github.com/repos/RarDog/Prisma/releases/latest';
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 releasesUrl = githubReleasesUrl;
@@ -111,7 +111,7 @@ class UpdateService {
'Authorization': 'token 1c744d6044d756759d7b1f693c94c80cf70d75fa',
}
: {
'User-Agent': 'Lunaris-App',
'User-Agent': 'Prisma-App',
'Accept': 'application/vnd.github+json',
},
);
@@ -239,10 +239,10 @@ class UpdateService {
final fromUrl =
parsed?.pathSegments.isEmpty ?? true ? '' : parsed!.pathSegments.last;
if (fromUrl.contains('.')) return fromUrl;
if (Platform.isAndroid) return 'Lunaris-v${info.version}.apk';
if (Platform.isLinux) return 'Lunaris-v${info.version}-linux-x64.tar.gz';
if (Platform.isWindows) return 'LunarisSetup-v${info.version}.exe';
return 'LunarisPortable-v${info.version}.zip';
if (Platform.isAndroid) return 'Prisma-v${info.version}.apk';
if (Platform.isLinux) return 'Prisma-v${info.version}-linux-x64.tar.gz';
if (Platform.isWindows) return 'PrismaSetup-v${info.version}.exe';
return 'PrismaPortable-v${info.version}.zip';
}
static String _versionFromTag(String tag) {
+1 -1
View File
@@ -40,7 +40,7 @@ class DioClient {
receiveTimeout: timeout,
sendTimeout: timeout,
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',
...?headers,
},
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -191,11 +192,13 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
if (_selectedTypes.isEmpty) return true;
final isVid = MediaUrlSelector.isVideo(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('gif') && isGif) return true;
if (_selectedTypes.contains('photo') && isPhoto) return true;
if (_selectedTypes.contains('text') && isText) return true;
return false;
}
@@ -205,94 +208,251 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
context: context,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: theme.colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (ctx) => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.6,
maxChildSize: 0.9,
initialChildSize: 0.65,
maxChildSize: 0.92,
builder: (_, scroll) => Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
padding: const EdgeInsets.fromLTRB(20, 4, 16, 12),
child: Row(
children: [
Icon(Icons.campaign_rounded, color: theme.colorScheme.primary),
const SizedBox(width: 10),
Text(
'Анонсы автора (${_announcements.length})',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
Container(
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(
'Личные сообщения и анонсы',
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(
child: ListView.separated(
controller: scroll,
padding: const EdgeInsets.all(16),
itemCount: _announcements.length,
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: [
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,
child: _announcements.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.mark_email_read_outlined,
size: 48,
color: theme.colorScheme.outline,
),
),
const SizedBox(height: 6),
FormattedContentText(
text: cleanContent.isNotEmpty ? cleanContent : a.content,
style: theme.textTheme.bodyMedium?.copyWith(
height: 1.45,
),
const SizedBox(height: 12),
Text(
'Нет сообщений или рассылок',
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
],
),
if (creatorLinks.isNotEmpty) ...[
const SizedBox(height: 10),
CreatorLinkChips(
links: creatorLinks,
title: 'Ссылки автора',
),
],
if (detectedCloudLinks.isNotEmpty) ...[
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 6,
children: detectedCloudLinks.map((link) {
return ActionChip(
avatar: Icon(
link.iconData,
size: 16,
color: link.brandColor,
)
: 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),
),
);
},
),
],
),
label: Text(
link.title.isNotEmpty ? link.title : link.serviceName,
style: const TextStyle(fontSize: 12),
const SizedBox(height: 10),
FormattedContentText(
text: displayContent,
style: theme.textTheme.bodyMedium?.copyWith(
height: 1.5,
letterSpacing: 0.15,
),
),
onPressed: () => launchUrl(
Uri.parse(link.url),
mode: LaunchMode.externalApplication,
),
);
}).toList(),
),
],
],
);
},
),
if (creatorLinks.isNotEmpty) ...[
const SizedBox(height: 12),
CreatorLinkChips(
links: creatorLinks,
title: 'Ссылки из сообщения',
),
],
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() {
final scheme = Theme.of(context).colorScheme;
final photoCount = _posts
.where((p) => !MediaUrlSelector.isVideo(p) && !MediaUrlSelector.isGif(p))
.where((p) =>
p.fileType != 'text' &&
!MediaUrlSelector.isVideo(p) &&
!MediaUrlSelector.isGif(p))
.length;
final videoCount = _posts.where(MediaUrlSelector.isVideo).length;
final gifCount = _posts.where(MediaUrlSelector.isGif).length;
final textCount = _posts.where((p) => p.fileType == 'text').length;
return Padding(
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');
}
});
},
),
],
],
),
),
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,8 @@
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -176,82 +179,154 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
// Search input bar if active
if (_isSearching)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: isRu
? 'Поиск по тегам и авторам...'
: 'Search by tags and artists...',
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: () {
_searchController.clear();
setState(() => _searchQuery = '');
},
)
: null,
isDense: true,
filled: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
child: ClipRRect(
borderRadius: BorderRadius.circular(18),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.surfaceContainerHigh
.withValues(alpha: 0.55)
: Colors.white.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
),
child: TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: isRu
? 'Поиск по тегам и авторам...'
: 'Search by tags and artists...',
prefixIcon: const Icon(Icons.search_rounded),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear_rounded),
onPressed: () {
HapticFeedback.lightImpact();
_searchController.clear();
setState(() => _searchQuery = '');
},
)
: null,
isDense: true,
filled: false,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
border: InputBorder.none,
),
onChanged: (val) =>
setState(() => _searchQuery = val.trim()),
),
),
),
onChanged: (val) => setState(() => _searchQuery = val.trim()),
),
),
// Navigation Tabs
// Navigation Tabs with Liquid Glass container
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 6),
child: SizedBox(
width: double.infinity,
child: SegmentedButton<int>(
showSelectedIcon: false,
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Icons.favorite_rounded, size: 18),
label: Text(
'${isRu ? 'Все' : 'All'} (${allPosts.length})',
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
ButtonSegment(
value: 1,
icon: const Icon(Icons.offline_pin_rounded, size: 18),
label: Text(
'${isRu ? 'Офлайн' : 'Offline'} (${offlinePosts.length})',
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
ButtonSegment(
value: 2,
icon: const Icon(Icons.groups_rounded, size: 18),
label: Text(
'${isRu ? 'Авторы' : 'Artists'} (${artistGroups.length})',
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
padding: const EdgeInsets.fromLTRB(16, 6, 16, 6),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(
alpha: Theme.of(context).brightness == Brightness.dark
? 0.20
: 0.04,
),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
selected: {_tabIndex},
onSelectionChanged: (value) {
setState(() {
_tabIndex = value.first;
_selectedArtist = null;
});
},
),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context)
.colorScheme
.surfaceContainerHigh
.withValues(alpha: 0.50)
: Colors.white.withValues(alpha: 0.72),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.12)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
),
child: SizedBox(
width: double.infinity,
child: SegmentedButton<int>(
showSelectedIcon: false,
style: ButtonStyle(
visualDensity: VisualDensity.compact,
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Icons.favorite_rounded, size: 18),
label: Text(
'${isRu ? 'Все' : 'All'} (${allPosts.length})',
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
ButtonSegment(
value: 1,
icon: const Icon(Icons.offline_pin_rounded,
size: 18),
label: Text(
'${isRu ? 'Офлайн' : 'Offline'} (${offlinePosts.length})',
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
ButtonSegment(
value: 2,
icon: const Icon(Icons.groups_rounded, size: 18),
label: Text(
'${isRu ? 'Авторы' : 'Artists'} (${artistGroups.length})',
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
),
],
selected: {_tabIndex},
onSelectionChanged: (value) {
HapticFeedback.selectionClick();
setState(() {
_tabIndex = value.first;
_selectedArtist = null;
});
},
),
),
),
),
),
),
),
@@ -265,9 +340,12 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
FilterChip(
label: Text(isRu ? 'Все' : 'All'),
selected: _mediaFilter == FavoriteMediaType.all,
onSelected: (_) => setState(
() => _mediaFilter = FavoriteMediaType.all,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _mediaFilter = FavoriteMediaType.all,
);
},
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 8),
@@ -275,9 +353,12 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
avatar: const Icon(Icons.videocam_rounded, size: 16),
label: Text(isRu ? 'Видео' : 'Videos'),
selected: _mediaFilter == FavoriteMediaType.video,
onSelected: (_) => setState(
() => _mediaFilter = FavoriteMediaType.video,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _mediaFilter = FavoriteMediaType.video,
);
},
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 8),
@@ -285,9 +366,12 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
avatar: const Icon(Icons.image_rounded, size: 16),
label: Text(isRu ? 'Фото' : 'Images'),
selected: _mediaFilter == FavoriteMediaType.image,
onSelected: (_) => setState(
() => _mediaFilter = FavoriteMediaType.image,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _mediaFilter = FavoriteMediaType.image,
);
},
visualDensity: VisualDensity.compact,
),
],
@@ -490,31 +574,40 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
}) {
final scheme = Theme.of(context).colorScheme;
return Container(
return _FavoritesLiquidCard(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: scheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.35),
),
),
glowColor: scheme.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
width: 40,
height: 40,
decoration: BoxDecoration(
color: scheme.primaryContainer.withValues(alpha: 0.7),
gradient: LinearGradient(
colors: [
scheme.primary,
scheme.primary.withValues(alpha: 0.85),
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: scheme.primary.withValues(alpha: 0.35),
blurRadius: 7,
offset: const Offset(0, 2.5),
),
],
),
child: Icon(
Icons.offline_pin_rounded,
color: scheme.onPrimaryContainer,
size: 22,
child: const Center(
child: Icon(
Icons.offline_pin_rounded,
color: Colors.white,
size: 22,
),
),
),
const SizedBox(width: 12),
@@ -526,6 +619,7 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
'$downloadedCount ${isRu ? 'из' : 'of'} $totalCount ${isRu ? 'офлайн' : 'saved'}',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
),
),
const SizedBox(height: 2),
@@ -553,21 +647,22 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
if (pendingCount > 0)
FilledButton.tonalIcon(
onPressed: () async {
HapticFeedback.mediumImpact();
final dm = ref.read(downloadManagerServiceProvider);
for (final p in pendingPosts) {
await dm.start(p);
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isRu
? 'В очередь загрузки добавлено: $pendingCount'
: 'Queued $pendingCount downloads',
),
behavior: SnackBarBehavior.floating,
SnackBar(
content: Text(
isRu
? 'В очередь загрузки добавлено: $pendingCount'
: 'Queued $pendingCount downloads',
),
);
behavior: SnackBarBehavior.floating,
),
);
},
icon: const Icon(Icons.download_rounded, size: 16),
label: Text(
@@ -594,7 +689,7 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
vertical: 6,
),
decoration: BoxDecoration(
color: scheme.secondaryContainer.withValues(alpha: 0.5),
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
),
child: Row(
@@ -610,8 +705,8 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
isRu ? 'Всё готово' : 'Synced',
style:
Theme.of(context).textTheme.labelSmall?.copyWith(
color: scheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
color: scheme.primary,
fontWeight: FontWeight.w700,
),
),
],
@@ -666,27 +761,36 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
ChoiceChip(
label: Text(isRu ? 'По дате' : 'Date'),
selected: _offlineSort == OfflineSortOption.dateAddedDesc,
onSelected: (_) => setState(
() => _offlineSort = OfflineSortOption.dateAddedDesc,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _offlineSort = OfflineSortOption.dateAddedDesc,
);
},
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 6),
ChoiceChip(
label: Text(isRu ? 'По размеру' : 'Size'),
selected: _offlineSort == OfflineSortOption.sizeDesc,
onSelected: (_) => setState(
() => _offlineSort = OfflineSortOption.sizeDesc,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _offlineSort = OfflineSortOption.sizeDesc,
);
},
visualDensity: VisualDensity.compact,
),
const SizedBox(width: 6),
ChoiceChip(
label: Text(isRu ? 'По автору' : 'Artist'),
selected: _offlineSort == OfflineSortOption.artist,
onSelected: (_) => setState(
() => _offlineSort = OfflineSortOption.artist,
),
onSelected: (_) {
HapticFeedback.selectionClick();
setState(
() => _offlineSort = OfflineSortOption.artist,
);
},
visualDensity: VisualDensity.compact,
),
],
@@ -712,38 +816,50 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
return Column(
children: [
// Artist Header Banner
Container(
// Artist Header Banner (Liquid Glass)
_FavoritesLiquidCard(
margin: const EdgeInsets.fromLTRB(16, 4, 16, 8),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context)
.colorScheme
.outlineVariant
.withValues(alpha: 0.3),
),
),
child: Row(
children: [
IconButton(
onPressed: () => setState(() => _selectedArtist = null),
onPressed: () {
HapticFeedback.selectionClick();
setState(() => _selectedArtist = null);
},
icon: const Icon(Icons.arrow_back_rounded),
tooltip: isRu ? 'Все авторы' : 'All artists',
),
const SizedBox(width: 8),
CircleAvatar(
radius: 18,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: Text(
selected.isNotEmpty ? selected[0].toUpperCase() : '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.tertiary,
],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.35),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Text(
selected.isNotEmpty ? selected[0].toUpperCase() : '?',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
@@ -756,6 +872,7 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
selected,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -776,6 +893,7 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
visualDensity: VisualDensity.compact,
),
onPressed: () {
HapticFeedback.selectionClick();
context.push('/search?q=${Uri.encodeComponent('artist:$selected')}');
},
icon: const Icon(Icons.search_rounded, size: 16),
@@ -808,7 +926,9 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
// Extended bottom padding so the last artist cards scroll comfortably above the floating dock
padding: EdgeInsets.fromLTRB(
16, 8, 16, 140 + MediaQuery.paddingOf(context).bottom),
itemCount: entries.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
@@ -863,6 +983,83 @@ class _FavoritesScreenState extends ConsumerState<FavoritesScreen> {
}
}
class _FavoritesLiquidCard extends StatelessWidget {
const _FavoritesLiquidCard({
required this.child,
this.padding,
this.glowColor,
this.margin,
});
final Widget child;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final Color? glowColor;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final accent = glowColor ?? theme.colorScheme.primary;
return Container(
margin: margin,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.30)
: accent.withValues(alpha: 0.07),
blurRadius: 16,
offset: const Offset(0, 5),
spreadRadius: -2,
),
BoxShadow(
color: accent.withValues(alpha: isDark ? 0.05 : 0.02),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.60),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: 0.38),
]
: [
Colors.white.withValues(alpha: 0.88),
Colors.white.withValues(alpha: 0.72),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.85),
width: 1.2,
),
),
child: child,
),
),
),
);
}
}
class _ArtistCard extends StatelessWidget {
const _ArtistCard({
required this.artistName,
@@ -881,123 +1078,182 @@ class _ArtistCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Card(
elevation: 0,
color: scheme.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: scheme.outlineVariant.withValues(alpha: 0.3),
),
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.28)
: scheme.primary.withValues(alpha: 0.06),
blurRadius: 14,
offset: const Offset(0, 4),
spreadRadius: -2,
),
],
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// Artist Avatar
Container(
width: 48,
height: 48,
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(22),
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
colors: [
scheme.primaryContainer,
scheme.tertiaryContainer,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
scheme.surfaceContainerHigh
.withValues(alpha: 0.58),
scheme.surfaceContainerLow
.withValues(alpha: 0.36),
]
: [
Colors.white.withValues(alpha: 0.88),
Colors.white.withValues(alpha: 0.72),
],
),
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
artistName.isNotEmpty ? artistName[0].toUpperCase() : '?',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: scheme.onPrimaryContainer,
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.13)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
),
),
const SizedBox(width: 12),
// Artist Name & Count
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Text(
artistName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
// Artist Squircle Avatar
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
width: 48,
height: 48,
decoration: BoxDecoration(
color: scheme.secondaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [
scheme.primary,
scheme.tertiary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: scheme.primary.withValues(alpha: 0.35),
blurRadius: 8,
offset: const Offset(0, 2.5),
),
],
),
child: Text(
'$postsCount ${isRu ? 'работ' : 'posts'}',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: scheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
// Thumbnails preview
if (previewUrls.isNotEmpty) ...[
const SizedBox(width: 8),
Row(
children: [
for (final url in previewUrls)
Padding(
padding: const EdgeInsets.only(left: 4),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 44,
height: 44,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
color: scheme.surfaceContainerHighest,
),
errorWidget: (_, __, ___) => Container(
color: scheme.surfaceContainerHighest,
child: const Icon(
Icons.image_not_supported_rounded,
size: 16,
),
),
),
child: Center(
child: Text(
artistName.isNotEmpty ? artistName[0].toUpperCase() : '?',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
// Artist Name & Count
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
artistName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 3),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$postsCount ${isRu ? 'работ' : 'posts'}',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: scheme.primary,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
// Thumbnails preview
if (previewUrls.isNotEmpty) ...[
const SizedBox(width: 8),
Row(
children: [
for (final url in previewUrls)
Padding(
padding: const EdgeInsets.only(left: 5),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.10)
: scheme.outlineVariant.withValues(alpha: 0.25),
),
borderRadius: BorderRadius.circular(10),
),
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
color: scheme.surfaceContainerHighest,
),
errorWidget: (_, __, ___) => Container(
color: scheme.surfaceContainerHighest,
child: const Icon(
Icons.image_not_supported_rounded,
size: 16,
),
),
),
),
),
),
],
),
],
const SizedBox(width: 4),
Icon(
Icons.chevron_right_rounded,
size: 20,
color: scheme.onSurfaceVariant.withValues(alpha: 0.5),
),
],
),
],
const SizedBox(width: 6),
Icon(
Icons.chevron_right_rounded,
color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
),
],
),
),
),
),
@@ -374,7 +374,11 @@ class _FeedScreenState extends ConsumerState<FeedScreen>
void _submitSearch(String query) {
final trimmed = query.trim();
if (trimmed.isEmpty) {
_clearFilters();
if (widget.initialQuery != null) {
context.go('/');
} else {
_applySearchQuery('');
}
return;
}
final location = '/?q=${Uri.encodeQueryComponent(trimmed)}';
@@ -649,7 +653,7 @@ class _FeedTitle extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Lunaris',
'Prisma',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w800,
),
@@ -104,7 +104,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
},
onCleared: () {
_query = '';
widget.onClearFilters();
widget.onSearch('');
},
),
),
@@ -235,7 +235,7 @@ class _FeedToolbarState extends State<FeedToolbar> {
},
onCleared: () {
_query = '';
widget.onClearFilters();
widget.onSearch('');
},
),
),
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 '../../../../shared/widgets/formatted_content_text.dart';
class CloudMirrorsCard extends StatelessWidget {
class CloudMirrorsCard extends StatefulWidget {
const CloudMirrorsCard({
super.key,
required this.links,
@@ -25,9 +25,21 @@ class CloudMirrorsCard extends StatelessWidget {
final ValueChanged<String>? onDownloadStream;
final bool initiallyExpanded;
@override
State<CloudMirrorsCard> createState() => _CloudMirrorsCardState();
}
class _CloudMirrorsCardState extends State<CloudMirrorsCard> {
int _selectedTab = 0; // 0: mirrors, 1: commentary
@override
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();
}
@@ -37,6 +49,14 @@ class CloudMirrorsCard extends StatelessWidget {
.whereType<String>()
.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(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
@@ -49,20 +69,18 @@ class CloudMirrorsCard extends StatelessWidget {
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
childrenPadding: const EdgeInsets.fromLTRB(14, 0, 14, 14),
initiallyExpanded: initiallyExpanded,
initiallyExpanded: widget.initiallyExpanded,
shape: const Border(),
collapsedShape: const Border(),
leading: Icon(
Icons.cloud_sync_rounded,
cardIcon,
color: theme.colorScheme.primary,
),
title: Row(
children: [
Expanded(
child: Text(
strings.ru
? 'Облачные диски и зеркала (${links.length})'
: 'Cloud Mirrors & Drives (${links.length})',
cardTitle,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
@@ -77,7 +95,8 @@ class CloudMirrorsCard extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.15),
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(
mainAxisSize: MainAxisSize.min,
@@ -86,7 +105,7 @@ class CloudMirrorsCard extends StatelessWidget {
size: 13, color: Colors.green),
const SizedBox(width: 4),
Text(
strings.ru ? 'Плеер' : 'Stream',
widget.strings.ru ? 'Плеер' : 'Stream',
style: theme.textTheme.labelSmall?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
@@ -116,7 +135,9 @@ class CloudMirrorsCard extends StatelessWidget {
Expanded(
child: Text.rich(
TextSpan(
text: strings.ru ? 'Пароль к архиву: ' : 'Archive Password: ',
text: widget.strings.ru
? 'Пароль к архиву: '
: 'Archive Password: ',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface,
),
@@ -135,13 +156,15 @@ class CloudMirrorsCard extends StatelessWidget {
IconButton(
icon: const Icon(Icons.copy_rounded, size: 16),
visualDensity: VisualDensity.compact,
tooltip: strings.ru ? 'Копировать пароль' : 'Copy Password',
tooltip: widget.strings.ru
? 'Копировать пароль'
: 'Copy Password',
onPressed: () {
Clipboard.setData(ClipboardData(text: detectedPassword));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
strings.ru
widget.strings.ru
? 'Пароль скопирован: $detectedPassword'
: 'Password copied: $detectedPassword',
),
@@ -157,63 +180,111 @@ class CloudMirrorsCard extends StatelessWidget {
const SizedBox(height: 10),
],
// Links list
if (links.isNotEmpty) ...[
...links.map((link) => _buildLinkTile(context, link, strings)),
],
// Commentary / Description
if (commentary != null && commentary!.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Divider(
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4),
height: 1,
),
const SizedBox(height: 10),
Row(
children: [
Icon(
Icons.notes_rounded,
size: 16,
color: theme.colorScheme.primary,
if (hasLinks && hasCommentary) ...[
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: SizedBox(
width: double.infinity,
child: SegmentedButton<int>(
segments: [
ButtonSegment(
value: 0,
icon: const Icon(Icons.cloud_queue_rounded, size: 16),
label: Text(
widget.strings.ru
? 'Зеркала (${links.length})'
: 'Mirrors (${links.length})',
),
),
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),
),
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(
builder: (context) {
final creatorLinks = CreatorLink.extractLinks(commentary);
if (creatorLinks.isEmpty) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(top: 10),
child: CreatorLinkChips(
links: creatorLinks,
title: strings.ru ? 'Ссылки автора' : 'Author Links',
),
);
},
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(
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(
BuildContext context,
CloudMediaLink link,
@@ -303,7 +374,7 @@ class CloudMirrorsCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Play button (if streamable)
if (link.isStreamable && onPlayStream != null) ...[
if (link.isStreamable && widget.onPlayStream != null) ...[
FilledButton.tonalIcon(
style: FilledButton.styleFrom(
visualDensity: VisualDensity.compact,
@@ -313,7 +384,7 @@ class CloudMirrorsCard extends StatelessWidget {
label: Text(strings.ru ? 'Смотреть' : 'Play'),
onPressed: () {
final target = link.directStreamUrl ?? link.url;
onPlayStream!(target);
widget.onPlayStream!(target);
},
),
const SizedBox(width: 8),
@@ -321,12 +392,13 @@ class CloudMirrorsCard extends StatelessWidget {
// Download button (if direct streamable and callback exists)
if (link.isStreamable &&
link.directStreamUrl != null &&
onDownloadStream != null) ...[
widget.onDownloadStream != null) ...[
IconButton.filledTonal(
visualDensity: VisualDensity.compact,
tooltip: strings.download,
icon: const Icon(Icons.download_rounded, size: 16),
onPressed: () => onDownloadStream!(link.directStreamUrl!),
onPressed: () =>
widget.onDownloadStream!(link.directStreamUrl!),
),
const SizedBox(width: 8),
],
File diff suppressed because it is too large Load Diff
@@ -298,7 +298,7 @@ class _TagGroupBlock extends StatelessWidget {
required this.onLongPress,
});
static const _collapsedLimit = 36;
static const _collapsedLimit = 28;
final String label;
final String group;
@@ -320,13 +320,51 @@ class _TagGroupBlock extends StatelessWidget {
children: [
Row(
children: [
Text(label, style: Theme.of(context).textTheme.labelLarge),
const SizedBox(width: 8),
Text(
'${tags.length}',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
Container(
width: 8,
height: 8,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: _headerDotColor(group, Theme.of(context)),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: _headerDotColor(group, Theme.of(context))
.withValues(alpha: 0.55),
blurRadius: 5,
spreadRadius: 1,
),
],
),
),
Text(
label,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.1,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1.5),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.12)
: Colors.black.withValues(alpha: 0.08),
),
),
child: Text(
'${tags.length}',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
@@ -338,6 +376,7 @@ class _TagGroupBlock extends StatelessWidget {
for (final tag in visibleTags)
TagChip(
tag: tag,
group: group,
onTap: () => onTap(tag),
onLongPress: () => onLongPress(tag),
),
@@ -360,4 +399,17 @@ class _TagGroupBlock extends StatelessWidget {
),
);
}
static Color _headerDotColor(String group, ThemeData theme) {
return switch (group.toLowerCase()) {
'artist' => const Color(0xFFFF4757),
'character' => const Color(0xFF2ED573),
'copyright' => const Color(0xFFA55EEA),
'species' => const Color(0xFFFFA502),
'meta' => const Color(0xFF1E90FF),
_ => theme.colorScheme.primary,
};
}
}
@@ -1,56 +1,334 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../backend/backend.dart';
import '../../../core/utils/result.dart';
import '../../../shared/widgets/adaptive_scaffold.dart';
import '../../../shared/widgets/error_view.dart';
import 'providers_controller.dart';
import 'widgets/provider_health_tile.dart';
class ProviderCheckScreen extends ConsumerWidget {
class ProviderCheckScreen extends ConsumerStatefulWidget {
const ProviderCheckScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<ProviderCheckScreen> createState() =>
_ProviderCheckScreenState();
}
class _ProviderCheckScreenState extends ConsumerState<ProviderCheckScreen> {
bool _isCheckingAll = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final providers = ref.watch(providersControllerProvider);
final health = ref.watch(providerHealthProvider);
final diagnostics = ref.watch(providerDiagnosticsProvider).value ?? {};
return AdaptiveScaffold(
title: 'Provider Check',
actions: [
IconButton(
tooltip: 'Check all',
onPressed: () => _checkAll(ref),
icon: const Icon(Icons.playlist_add_check_rounded),
final bottomInset = MediaQuery.paddingOf(context).bottom;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: AppBar(
backgroundColor: isDark
? theme.colorScheme.surface.withValues(alpha: 0.75)
: Colors.white.withValues(alpha: 0.85),
surfaceTintColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
onPressed: () {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
context.go('/providers');
}
},
tooltip: isRu ? 'Назад' : 'Back',
),
title: Text(
isRu ? 'Проверка источников' : 'Provider Check',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 12),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: _isCheckingAll ? null : () => _checkAll(ref),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: const Color(0xFF6366F1)
.withValues(alpha: 0.35),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_isCheckingAll)
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
else
const Icon(
Icons.playlist_add_check_rounded,
size: 16,
color: Colors.white,
),
const SizedBox(width: 5),
Text(
isRu ? 'Проверить все' : 'Check All',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
),
),
),
),
],
),
),
),
],
),
body: providers.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => ErrorView(message: error.toString()),
data: (items) => ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ProviderHealthTile(
config: item,
health: health[item.id],
diagnostics: diagnostics[item.id],
onCheck: () => _checkOne(ref, item.id),
);
},
),
data: (items) {
int onlineCount = 0;
int offlineCount = 0;
int pingSum = 0;
int pingCount = 0;
for (final item in items) {
final h = health[item.id];
if (h != null) {
if (h.status == ProviderStatus.online) {
onlineCount++;
pingSum += h.pingMs;
pingCount++;
} else if (h.status == ProviderStatus.offline) {
offlineCount++;
}
}
}
final avgPing =
pingCount > 0 ? (pingSum / pingCount).round() : null;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: ListView(
padding: EdgeInsets.fromLTRB(
16,
MediaQuery.paddingOf(context).top + 68,
16,
140 + bottomInset,
),
children: [
// Status Overview Card
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.65),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: 0.40),
]
: [
Colors.white.withValues(alpha: 0.90),
Colors.white.withValues(alpha: 0.75),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.25)
: const Color(0xFF6366F1).withValues(alpha: 0.08),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 8,
runSpacing: 6,
children: [
Text(
isRu ? 'Состояние сети' : 'Network Health',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
if (avgPing != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withValues(
alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF10B981).withValues(
alpha: isDark ? 0.38 : 0.25),
),
),
child: Text(
'${isRu ? 'Средний пинг' : 'Avg ping'}: $avgPing ms',
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFF10B981),
fontFamily: 'monospace',
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _MetricPill(
label: isRu ? 'Всего' : 'Total',
value: '${items.length}',
color: theme.colorScheme.primary,
isDark: isDark,
),
),
const SizedBox(width: 8),
Expanded(
child: _MetricPill(
label: isRu ? 'Онлайн' : 'Online',
value: '$onlineCount',
color: const Color(0xFF10B981),
isDark: isDark,
),
),
const SizedBox(width: 8),
Expanded(
child: _MetricPill(
label: isRu ? 'Офлайн' : 'Offline',
value: '$offlineCount',
color: const Color(0xFFEF4444),
isDark: isDark,
),
),
],
),
],
),
),
// Health Tiles List
if (items.isEmpty)
Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: isDark
? theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.40)
: Colors.white.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(24),
),
child: Center(
child: Text(
isRu ? 'Нет источников для проверки' : 'No providers to check',
style: theme.textTheme.bodyMedium,
),
),
)
else
...items.map((item) {
return ProviderHealthTile(
key: ValueKey(item.id),
config: item,
health: health[item.id],
diagnostics: diagnostics[item.id],
onCheck: () => _checkOne(ref, item.id),
);
}),
],
),
),
);
},
),
);
}
Future<void> _checkAll(WidgetRef ref) async {
final result = await ref.read(providerCheckServiceProvider).checkAll();
if (result is Success<List<ProviderHealth>>) {
ref.read(providerHealthProvider.notifier).state = {
for (final item in result.data) item.providerId: item,
};
setState(() => _isCheckingAll = true);
HapticFeedback.mediumImpact();
try {
final result =
await ref.read(providerCheckServiceProvider).checkAll();
if (result is Success<List<ProviderHealth>>) {
ref.read(providerHealthProvider.notifier).state = {
for (final item in result.data) item.providerId: item,
};
}
} finally {
if (mounted) setState(() => _isCheckingAll = false);
}
}
@@ -65,3 +343,53 @@ class ProviderCheckScreen extends ConsumerWidget {
}
}
}
class _MetricPill extends StatelessWidget {
const _MetricPill({
required this.label,
required this.value,
required this.color,
required this.isDark,
});
final String label;
final String value;
final Color color;
final bool isDark;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: color.withValues(alpha: isDark ? 0.12 : 0.08),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: color.withValues(alpha: isDark ? 0.30 : 0.20),
width: 1,
),
),
child: Column(
children: [
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: color,
),
),
const SizedBox(height: 1),
Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: color.withValues(alpha: 0.85),
),
),
],
),
);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,9 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../shared/widgets/adaptive_scaffold.dart';
import '../../../shared/widgets/confirm_dialog.dart';
import '../../../shared/widgets/error_view.dart';
import 'providers_controller.dart';
@@ -13,50 +14,371 @@ class ProvidersScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final providers = ref.watch(providersControllerProvider);
return AdaptiveScaffold(
title: 'Providers',
actions: [
IconButton(
tooltip: 'Check providers',
onPressed: () => context.go('/providers/check'),
icon: const Icon(Icons.network_check_rounded),
final bottomInset = MediaQuery.paddingOf(context).bottom;
return Scaffold(
extendBodyBehindAppBar: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(60),
child: ClipRRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: AppBar(
backgroundColor: isDark
? theme.colorScheme.surface.withValues(alpha: 0.72)
: Colors.white.withValues(alpha: 0.82),
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: false,
leading: Navigator.of(context).canPop()
? IconButton(
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20),
onPressed: () => context.pop(),
tooltip: isRu ? 'Назад' : 'Back',
)
: null,
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(9),
boxShadow: [
BoxShadow(
color:
const Color(0xFF6366F1).withValues(alpha: 0.35),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Center(
child:
Icon(Icons.hub_rounded, size: 16, color: Colors.white),
),
),
const SizedBox(width: 8),
Flexible(
child: Text(
isRu ? 'Провайдеры' : 'Providers',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
actions: [
// Provider Check Glass Button
Padding(
padding: const EdgeInsets.only(right: 6),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () {
HapticFeedback.lightImpact();
context.push('/providers/check');
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: theme.colorScheme.primary
.withValues(alpha: isDark ? 0.35 : 0.22),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.network_check_rounded,
size: 16,
color: theme.colorScheme.primary,
),
const SizedBox(width: 5),
Text(
isRu ? 'Проверка' : 'Check',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: theme.colorScheme.primary,
),
),
],
),
),
),
),
),
// Add Provider Accent Button
Padding(
padding: const EdgeInsets.only(right: 12),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () {
HapticFeedback.lightImpact();
context.push('/providers/new');
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: const Color(0xFF6366F1)
.withValues(alpha: 0.35),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.add_rounded,
size: 16,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
isRu ? 'Создать' : 'Add',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
),
),
),
),
],
),
),
),
IconButton(
tooltip: 'Add provider',
onPressed: () => context.go('/providers/new'),
icon: const Icon(Icons.add_rounded),
),
],
),
body: providers.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => ErrorView(message: error.toString()),
data: (items) => ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ProviderCard(
config: item,
onToggle: (enabled) => ref
.read(providersControllerProvider.notifier)
.toggle(item.id, enabled),
onEdit: () => context.go('/providers/new', extra: item),
onDelete: () async {
final ok = await showConfirmDialog(
context,
title: 'Delete provider',
message: 'Remove ${item.name}?',
);
if (ok) {
await ref
.read(providersControllerProvider.notifier)
.delete(item.id);
}
},
);
},
),
data: (items) {
final activeCount = items.where((p) => p.enabled).length;
return Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: ListView(
padding: EdgeInsets.fromLTRB(
16,
MediaQuery.paddingOf(context).top + 68,
16,
140 + bottomInset,
),
children: [
// Overview Glass Card
Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.65),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: 0.40),
]
: [
Colors.white.withValues(alpha: 0.90),
Colors.white.withValues(alpha: 0.75),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.25)
: const Color(0xFF6366F1).withValues(alpha: 0.08),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isRu
? 'Управление источниками'
: 'Provider Management',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
const SizedBox(height: 4),
Text(
isRu
? 'Настройте подключение, API ключи и приоритеты booru-сервисов'
: 'Configure connections, API keys and search priorities',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF10B981)
.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: const Color(0xFF10B981)
.withValues(alpha: isDark ? 0.40 : 0.28),
),
),
child: Text(
'$activeCount / ${items.length} ${isRu ? 'вкл' : 'on'}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Color(0xFF10B981),
),
),
),
],
),
),
// Empty State
if (items.isEmpty)
Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: isDark
? theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.40)
: Colors.white.withValues(alpha: 0.80),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.10)
: Colors.white,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.cloud_off_rounded,
size: 48,
color: theme.colorScheme.onSurfaceVariant
.withValues(alpha: 0.5),
),
const SizedBox(height: 12),
Text(
isRu
? 'Нет настроенных источников'
: 'No providers configured',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
Text(
isRu
? 'Нажмите «Создать», чтобы добавить новый booru-провайдер'
: 'Tap "Add" to set up a new booru provider',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
)
else
// List of Provider Cards
...items.map((item) {
return ProviderCard(
key: ValueKey(item.id),
config: item,
onToggle: (enabled) => ref
.read(providersControllerProvider.notifier)
.toggle(item.id, enabled),
onEdit: () => context.push(
'/providers/new',
extra: item,
),
onDelete: () async {
final ok = await showConfirmDialog(
context,
title: isRu ? 'Удалить источник' : 'Delete provider',
message: isRu
? 'Вы действительно хотите удалить ${item.name}?'
: 'Remove ${item.name}?',
);
if (ok) {
await ref
.read(providersControllerProvider.notifier)
.delete(item.id);
}
},
);
}),
],
),
),
);
},
),
);
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../backend/backend.dart';
@@ -18,27 +19,396 @@ class ProviderCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: Switch(value: config.enabled, onChanged: onToggle),
title: Text(config.name),
subtitle: Text(
'${config.apiType} · ${config.baseUrl}\nPriority ${config.priority}'),
isThreeLine: true,
trailing: Wrap(
spacing: 4,
children: [
IconButton(
tooltip: 'Edit',
onPressed: onEdit,
icon: const Icon(Icons.edit_rounded),
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final (engineColor, engineIcon, engineTitle) = _engineInfo(config.apiType);
return Container(
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.30)
: engineColor.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 6),
spreadRadius: -2,
),
BoxShadow(
color: engineColor.withValues(alpha: isDark ? 0.05 : 0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: config.enabled ? 0.65 : 0.35),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: config.enabled ? 0.45 : 0.20),
]
: [
Colors.white
.withValues(alpha: config.enabled ? 0.90 : 0.60),
Colors.white
.withValues(alpha: config.enabled ? 0.75 : 0.45),
],
),
IconButton(
tooltip: 'Delete',
onPressed: onDelete,
icon: const Icon(Icons.delete_rounded),
border: Border.all(
color: isDark
? (config.enabled
? engineColor.withValues(alpha: 0.28)
: Colors.white.withValues(alpha: 0.08))
: (config.enabled
? Colors.white.withValues(alpha: 0.90)
: Colors.white.withValues(alpha: 0.50)),
width: 1.2,
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Row 1: Icon squircle + Name & details + Switch
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Engine Icon Squircle Badge
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: config.enabled
? [
engineColor,
engineColor.withValues(alpha: 0.80),
]
: [
Colors.grey.shade600,
Colors.grey.shade700,
],
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
if (config.enabled)
BoxShadow(
color: engineColor.withValues(alpha: 0.38),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: Icon(
engineIcon,
size: 22,
color: Colors.white,
),
),
),
const SizedBox(width: 14),
// Name & Sub-details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
config.name,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
color: config.enabled
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface
.withValues(alpha: 0.6),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 5),
Wrap(
spacing: 6,
runSpacing: 4,
children: [
// Engine badge
_GlassPill(
label: engineTitle,
color: engineColor,
isDark: isDark,
),
// Priority badge
_GlassPill(
label: '${isRu ? 'Приоритет' : 'Priority'} ${config.priority}',
color: theme.colorScheme.primary,
isDark: isDark,
),
],
),
],
),
),
// Switch
Transform.scale(
scale: 0.9,
child: Switch.adaptive(
value: config.enabled,
onChanged: (val) {
HapticFeedback.selectionClick();
onToggle(val);
},
activeThumbColor: engineColor,
activeTrackColor: engineColor.withValues(alpha: 0.45),
),
),
],
),
const SizedBox(height: 12),
// Row 2: Base URL glass chip
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: isDark
? Colors.black.withValues(alpha: 0.20)
: theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.black.withValues(alpha: 0.05),
),
),
child: Row(
children: [
Icon(
Icons.link_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant
.withValues(alpha: 0.8),
),
const SizedBox(width: 6),
Expanded(
child: Text(
config.baseUrl,
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: theme.colorScheme.onSurfaceVariant,
letterSpacing: -0.1,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
// Row 3: Action Buttons (Edit & Delete)
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Edit Action Button
_GlassActionButton(
icon: Icons.edit_rounded,
label: isRu ? 'Изменить' : 'Edit',
color: theme.colorScheme.primary,
isDark: isDark,
onTap: () {
HapticFeedback.lightImpact();
onEdit();
},
),
const SizedBox(width: 8),
// Delete Action Button
_GlassActionButton(
icon: Icons.delete_outline_rounded,
label: isRu ? 'Удалить' : 'Delete',
color: const Color(0xFFEF4444),
isDark: isDark,
onTap: () {
HapticFeedback.lightImpact();
onDelete();
},
),
],
),
],
),
),
),
);
}
static (Color, IconData, String) _engineInfo(String apiType) {
return switch (apiType.toLowerCase()) {
'gelbooru' => (
const Color(0xFF10B981),
Icons.image_search_rounded,
'Gelbooru',
),
'rule34' => (
const Color(0xFF84CC16),
Icons.photo_library_rounded,
'Rule34',
),
'paheal' => (
const Color(0xFFF97316),
Icons.filter_hdr_rounded,
'Paheal HTML',
),
'realbooru_html' => (
const Color(0xFFEAB308),
Icons.camera_alt_rounded,
'Realbooru HTML',
),
'danbooru' => (
const Color(0xFF3B82F6),
Icons.auto_awesome_rounded,
'Danbooru',
),
'moebooru' => (
const Color(0xFF8B5CF6),
Icons.face_retouching_natural_rounded,
'Moebooru',
),
'e621' => (
const Color(0xFFF59E0B),
Icons.pets_rounded,
'e621 / e926',
),
'kemono' => (
const Color(0xFFEC4899),
Icons.palette_rounded,
'Kemono',
),
'coomer' => (
const Color(0xFF06B6D4),
Icons.collections_rounded,
'Coomer',
),
_ => (
const Color(0xFF6366F1),
Icons.hub_rounded,
apiType.isEmpty ? 'Custom' : apiType,
),
};
}
}
class _GlassPill extends StatelessWidget {
const _GlassPill({
required this.label,
required this.color,
required this.isDark,
});
final String label;
final Color color;
final bool isDark;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: color.withValues(alpha: isDark ? 0.35 : 0.25),
width: 1,
),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: color,
letterSpacing: -0.2,
),
),
);
}
}
class _GlassActionButton extends StatelessWidget {
const _GlassActionButton({
required this.icon,
required this.label,
required this.color,
required this.isDark,
required this.onTap,
});
final IconData icon;
final String label;
final Color color;
final bool isDark;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(12),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withValues(alpha: isDark ? 0.14 : 0.08),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withValues(alpha: isDark ? 0.32 : 0.22),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: color),
const SizedBox(width: 5),
Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
letterSpacing: -0.1,
),
),
],
),
),
),
);
@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../backend/backend.dart';
import '../../../../shared/widgets/provider_status_badge.dart';
class ProviderHealthTile extends StatelessWidget {
class ProviderHealthTile extends StatefulWidget {
const ProviderHealthTile({
required this.config,
required this.health,
@@ -17,59 +18,408 @@ class ProviderHealthTile extends StatelessWidget {
final ProviderDiagnostics? diagnostics;
final VoidCallback onCheck;
@override
State<ProviderHealthTile> createState() => _ProviderHealthTileState();
}
class _ProviderHealthTileState extends State<ProviderHealthTile> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final config = widget.config;
final health = widget.health;
final diagnostics = widget.diagnostics;
final hint = ProviderQualityHint.from(
config: config,
health: health,
diagnostics: diagnostics,
isRu: isRu,
);
return Card(
child: ExpansionTile(
leading: ProviderStatusBadge(health: health),
title: Text(config.name),
subtitle: Wrap(
spacing: 8,
runSpacing: 6,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(
avatar: Icon(hint.icon, size: 16),
label: Text(hint.label),
visualDensity: VisualDensity.compact,
backgroundColor: hint.color(context).withValues(alpha: 0.16),
side:
BorderSide(color: hint.color(context).withValues(alpha: 0.4)),
),
Text('${config.apiType} | priority ${config.priority}'),
],
),
trailing: IconButton(
tooltip: 'Check',
onPressed: onCheck,
icon: const Icon(Icons.network_check_rounded),
),
childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
children: [
Align(
alignment: Alignment.centerLeft,
child: Text(
[
'${config.enabled ? 'Enabled' : 'Disabled'} | ${config.baseUrl}',
if (health != null) 'Ping ${health!.pingMs} ms',
if (health?.apiVersion != null) 'API ${health!.apiVersion}',
if (health?.lastCheckedAt != null)
'Checked ${health!.lastCheckedAt}',
if (diagnostics != null)
'Last search: ${diagnostics!.lastResultCount} posts at ${diagnostics!.lastSearchAt}',
if (diagnostics?.lastErrorMessage != null)
diagnostics!.lastErrorMessage!,
if (health?.errorMessage != null) health!.errorMessage!,
].join('\n'),
),
final status = health?.status ?? ProviderStatus.unknown;
final statusColor = switch (status) {
ProviderStatus.online => (health != null && health.pingMs > 2500)
? const Color(0xFFF59E0B)
: const Color(0xFF10B981),
ProviderStatus.offline => const Color(0xFFEF4444),
ProviderStatus.unknown => const Color(0xFF94A3B8),
};
final hasError = diagnostics?.lastErrorMessage != null ||
health?.errorMessage != null;
final errorMessage = diagnostics?.lastErrorMessage ?? health?.errorMessage;
return Container(
margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.30)
: statusColor.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 6),
spreadRadius: -2,
),
BoxShadow(
color: statusColor.withValues(alpha: isDark ? 0.06 : 0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(22),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutCubic,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.65),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: 0.40),
]
: [
Colors.white.withValues(alpha: 0.92),
Colors.white.withValues(alpha: 0.78),
],
),
border: Border.all(
color: isDark
? statusColor.withValues(alpha: 0.30)
: Colors.white.withValues(alpha: 0.90),
width: 1.2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Tile
InkWell(
onTap: () {
HapticFeedback.selectionClick();
setState(() => _expanded = !_expanded);
},
borderRadius: BorderRadius.circular(22),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Status Badge with glowing LED
ProviderStatusBadge(health: health),
const SizedBox(width: 12),
// Name & Badges
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
config.name,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 5),
Wrap(
spacing: 6,
runSpacing: 4,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
// Ping chip if available
if (health != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2.5),
decoration: BoxDecoration(
color: statusColor.withValues(
alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: statusColor.withValues(
alpha: isDark ? 0.38 : 0.25),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bolt_rounded,
size: 12,
color: statusColor,
),
const SizedBox(width: 3),
Text(
'${health.pingMs} ms',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: statusColor,
fontFamily: 'monospace',
),
),
],
),
),
// Quality Hint Chip
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2.5),
decoration: BoxDecoration(
color: hint.color(context).withValues(
alpha: isDark ? 0.14 : 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: hint.color(context).withValues(
alpha: isDark ? 0.32 : 0.20),
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
hint.icon,
size: 12,
color: hint.color(context),
),
const SizedBox(width: 4),
Text(
hint.label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: hint.color(context),
letterSpacing: -0.1,
),
),
],
),
),
],
),
],
),
),
// Check Button
IconButton(
tooltip: isRu ? 'Проверить' : 'Check',
onPressed: () {
HapticFeedback.lightImpact();
widget.onCheck();
},
icon: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: theme.colorScheme.primary
.withValues(alpha: isDark ? 0.18 : 0.10),
shape: BoxShape.circle,
border: Border.all(
color: theme.colorScheme.primary
.withValues(alpha: isDark ? 0.40 : 0.25),
),
),
child: Center(
child: Icon(
Icons.refresh_rounded,
size: 18,
color: theme.colorScheme.primary,
),
),
),
),
// Expand Chevron
Icon(
_expanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
size: 20,
color: theme.colorScheme.onSurfaceVariant,
),
],
),
),
),
// Expanded Diagnostics Section
if (_expanded) ...[
Divider(
height: 1,
thickness: 1,
color: isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.black.withValues(alpha: 0.05),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Diagnostic row 1: URL & Enabled
_DiagItem(
icon: Icons.link_rounded,
label: isRu ? 'Сервер' : 'Endpoint',
value: '${config.baseUrl} (${config.enabled ? (isRu ? 'Активен' : 'Enabled') : (isRu ? 'Выключен' : 'Disabled')})',
isMonospace: true,
),
const SizedBox(height: 8),
// Diagnostic row 2: API Type & Priority
_DiagItem(
icon: Icons.hub_outlined,
label: isRu ? 'Движок и приоритет' : 'Engine & Priority',
value: '${config.apiType} · ${isRu ? 'приоритет' : 'priority'} ${config.priority}',
),
const SizedBox(height: 8),
// Diagnostic row 3: API Version
if (health?.apiVersion != null) ...[
_DiagItem(
icon: Icons.info_outline_rounded,
label: isRu ? 'Версия API' : 'API Version',
value: health!.apiVersion!,
),
const SizedBox(height: 8),
],
// Diagnostic row 4: Last Checked Time
if (health?.lastCheckedAt != null) ...[
_DiagItem(
icon: Icons.schedule_rounded,
label: isRu ? 'Проверено' : 'Last Checked',
value: health!.lastCheckedAt.toString().split('.').first,
isMonospace: true,
),
const SizedBox(height: 8),
],
// Diagnostic row 5: Last Search
if (diagnostics != null) ...[
_DiagItem(
icon: Icons.search_rounded,
label: isRu ? 'Последний поиск' : 'Last Search',
value: isRu
? '${diagnostics.lastResultCount} постов получено в ${diagnostics.lastSearchAt.toString().split('.').first}'
: '${diagnostics.lastResultCount} posts retrieved at ${diagnostics.lastSearchAt.toString().split('.').first}',
),
const SizedBox(height: 8),
],
// Error Box (if any)
if (hasError && errorMessage != null) ...[
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEF4444)
.withValues(alpha: isDark ? 0.16 : 0.08),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: const Color(0xFFEF4444)
.withValues(alpha: isDark ? 0.38 : 0.25),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icons.error_outline_rounded,
size: 16,
color: Color(0xFFEF4444),
),
const SizedBox(width: 8),
Expanded(
child: Text(
errorMessage,
style: const TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: Color(0xFFEF4444),
height: 1.35,
),
),
),
],
),
),
],
],
),
),
],
],
),
),
),
);
}
}
class _DiagItem extends StatelessWidget {
const _DiagItem({
required this.icon,
required this.label,
required this.value,
this.isMonospace = false,
});
final IconData icon;
final String label;
final String value;
final bool isMonospace;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
icon,
size: 15,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
const SizedBox(width: 8),
Text(
'$label: ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurfaceVariant,
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 12,
fontFamily: isMonospace ? 'monospace' : null,
color: theme.colorScheme.onSurface,
),
),
),
],
);
}
}
@@ -87,10 +437,11 @@ class ProviderQualityHint {
required ContentProviderConfig config,
required ProviderHealth? health,
required ProviderDiagnostics? diagnostics,
required bool isRu,
}) {
if (!config.enabled) {
return ProviderQualityHint._(
'Disabled',
isRu ? 'Выключен' : 'Disabled',
Icons.pause_circle_outline_rounded,
(scheme) => scheme.outline,
);
@@ -98,34 +449,34 @@ class ProviderQualityHint {
if (diagnostics?.lastErrorMessage != null ||
health?.status == ProviderStatus.offline) {
return ProviderQualityHint._(
'Search failing',
isRu ? 'Ошибка поиска' : 'Search failing',
Icons.error_outline_rounded,
(scheme) => scheme.error,
);
}
if (diagnostics != null && diagnostics.lastResultCount == 0) {
return ProviderQualityHint._(
'No recent results',
isRu ? 'Нет результатов' : 'No recent results',
Icons.search_off_rounded,
(scheme) => scheme.tertiary,
);
}
if (health != null && health.pingMs > 2500) {
return ProviderQualityHint._(
'Slow',
isRu ? 'Медленно' : 'Slow',
Icons.speed_rounded,
(scheme) => scheme.secondary,
);
}
if (health != null || diagnostics != null) {
return ProviderQualityHint._(
'Healthy',
isRu ? 'Стабилен' : 'Healthy',
Icons.check_circle_outline_rounded,
(scheme) => scheme.primary,
);
}
return ProviderQualityHint._(
'Not checked',
isRu ? 'Не проверен' : 'Not checked',
Icons.help_outline_rounded,
(scheme) => scheme.outline,
);
@@ -1,4 +1,7 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -47,6 +50,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
final strings = AppStrings(settings.languageCode);
final isRu = strings.ru;
final theme = Theme.of(context);
final bottomInset = MediaQuery.paddingOf(context).bottom;
return AdaptiveScaffold(
title: strings.search,
@@ -78,19 +82,12 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 860),
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
// Extended bottom padding so the cheat sheet card is never obscured by the floating dock
padding: EdgeInsets.fromLTRB(16, 14, 16, 140 + bottomInset),
children: [
// 1. Search Bar with Quick Operators
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: theme.colorScheme.outlineVariant
.withValues(alpha: 0.3),
),
),
_SearchLiquidCard(
glowColor: theme.colorScheme.primary,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -171,56 +168,124 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
const SizedBox(height: 16),
// 2. Recent Searches Card
_SearchSectionCard(
icon: Icons.history_rounded,
accentColor: const Color(0xFF6366F1),
title: isRu ? 'Недавние запросы' : 'Recent searches',
badgeCount: data.recent
.map((e) => e.query.trim().toLowerCase())
.where((q) => q.isNotEmpty)
.toSet()
.length,
trailing: data.recent.isEmpty
? null
: TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
_SearchLiquidCard(
glowColor: const Color(0xFF6366F1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const _SearchIconBadge(
icon: Icons.history_rounded,
color: Color(0xFF6366F1),
),
onPressed: () => ref
.read(searchControllerProvider.notifier)
.clearHistory(),
child: Text(
isRu ? 'Очистить' : 'Clear',
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 13,
fontWeight: FontWeight.w600,
const SizedBox(width: 12),
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
isRu ? 'Недавние запросы' : 'Recent searches',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 15,
letterSpacing: -0.2,
),
),
),
if (data.recent.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFF6366F1)
.withValues(alpha: 0.16),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${data.recent.map((e) => e.query.trim().toLowerCase()).where((q) => q.isNotEmpty).toSet().length}',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
color: const Color(0xFF6366F1),
),
),
),
],
],
),
),
),
child: RecentSearches(
items: data.recent,
onTap: (query) =>
context.go('/?q=${Uri.encodeQueryComponent(query)}'),
onDelete: (id) => ref
.read(searchControllerProvider.notifier)
.deleteHistory(id),
if (data.recent.isNotEmpty)
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
HapticFeedback.selectionClick();
ref
.read(searchControllerProvider.notifier)
.clearHistory();
},
child: Text(
isRu ? 'Очистить' : 'Clear',
style: TextStyle(
color: theme.colorScheme.primary,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 14),
RecentSearches(
items: data.recent,
onTap: (query) =>
context.go('/?q=${Uri.encodeQueryComponent(query)}'),
onDelete: (id) => ref
.read(searchControllerProvider.notifier)
.deleteHistory(id),
),
],
),
),
const SizedBox(height: 16),
// 3. Explore Popular Categories & Tags
_SearchSectionCard(
icon: Icons.explore_rounded,
accentColor: const Color(0xFF10B981),
title: isRu ? 'Популярные категории' : 'Explore categories',
_SearchLiquidCard(
glowColor: const Color(0xFF10B981),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const _SearchIconBadge(
icon: Icons.explore_rounded,
color: Color(0xFF10B981),
),
const SizedBox(width: 12),
Expanded(
child: Text(
isRu ? 'Популярные категории' : 'Explore categories',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 15,
letterSpacing: -0.2,
),
),
),
],
),
const SizedBox(height: 16),
_CategoryTagRow(
categoryName: isRu ? 'Тематика' : 'Theme & Ambience',
color: const Color(0xFF0EA5E9),
@@ -234,7 +299,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
],
onTap: (tag) => _onTagTap(tag, data.query),
),
const SizedBox(height: 14),
const SizedBox(height: 16),
_CategoryTagRow(
categoryName:
isRu ? 'Персонажи и детали' : 'Characters & Attire',
@@ -250,7 +315,7 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
],
onTap: (tag) => _onTagTap(tag, data.query),
),
const SizedBox(height: 14),
const SizedBox(height: 16),
_CategoryTagRow(
categoryName: isRu ? 'Провайдеры' : 'Providers',
color: const Color(0xFF8B5CF6),
@@ -268,84 +333,78 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
),
const SizedBox(height: 16),
// 4. Search Tips / Cheat Sheet Card
Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: theme.colorScheme.outlineVariant
.withValues(alpha: 0.3),
),
),
child: ExpansionTile(
shape: const Border(),
collapsedShape: const Border(),
leading: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: const Color(0xFFF59E0B).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.lightbulb_outline_rounded,
size: 18,
// 4. Search Tips / Cheat Sheet Card (Full Liquid Glass Container)
_SearchLiquidCard(
padding: EdgeInsets.zero,
glowColor: const Color(0xFFF59E0B),
child: Theme(
data: theme.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
shape: const Border(),
collapsedShape: const Border(),
tilePadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4),
leading: const _SearchIconBadge(
icon: Icons.lightbulb_rounded,
color: Color(0xFFF59E0B),
),
),
title: Text(
isRu ? 'Шпаргалка по поиску' : 'Search tips & operators',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
),
),
initiallyExpanded: _showTips,
onExpansionChanged: (exp) => setState(() => _showTips = exp),
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_TipRow(
code: 'tag_a and tag_b',
description: isRu
? 'Независимый опрос обоих тегов с чередованием постов в ленте.'
: 'Interleaves results from both queries independently.',
),
const SizedBox(height: 8),
_TipRow(
code: 'type:video / type:gif',
description: isRu
? 'Показывает только видео или анимированные GIF.'
: 'Filters results to only videos or animated GIFs.',
),
const SizedBox(height: 8),
_TipRow(
code: 'rating:safe / rating:explicit',
description: isRu
? 'Фильтр по возрастному рейтингу медиа.'
: 'Filter by age rating classification.',
),
const SizedBox(height: 8),
_TipRow(
code: 'score:>50 / score:>100',
description: isRu
? 'Посты с оценкой пользователей выше указанной.'
: 'Posts with community score greater than threshold.',
),
const SizedBox(height: 8),
_TipRow(
code: 'provider:gelbooru',
description: isRu
? 'Поиск исключительно в указанном источнике.'
: 'Search specifically on the given booru provider.',
),
],
title: Text(
isRu ? 'Шпаргалка по поиску' : 'Search tips & operators',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
],
initiallyExpanded: _showTips,
onExpansionChanged: (exp) {
HapticFeedback.selectionClick();
setState(() => _showTips = exp);
},
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_TipRow(
code: 'tag_a and tag_b',
description: isRu
? 'Независимый опрос обоих тегов с чередованием постов в ленте.'
: 'Interleaves results from both queries independently.',
),
const SizedBox(height: 8),
_TipRow(
code: 'type:video / type:gif',
description: isRu
? 'Показывает только видео или анимированные GIF.'
: 'Filters results to only videos or animated GIFs.',
),
const SizedBox(height: 8),
_TipRow(
code: 'rating:safe / rating:explicit',
description: isRu
? 'Фильтр по возрастному рейтингу медиа.'
: 'Filter by age rating classification.',
),
const SizedBox(height: 8),
_TipRow(
code: 'score:>50 / score:>100',
description: isRu
? 'Посты с оценкой пользователей выше указанной.'
: 'Posts with community score greater than threshold.',
),
const SizedBox(height: 8),
_TipRow(
code: 'provider:gelbooru',
description: isRu
? 'Поиск исключительно в указанном источнике.'
: 'Search specifically on the given booru provider.',
),
],
),
),
],
),
),
),
const SizedBox(height: 24),
@@ -358,93 +417,117 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
}
}
class _SearchSectionCard extends StatelessWidget {
const _SearchSectionCard({
required this.icon,
required this.accentColor,
required this.title,
class _SearchLiquidCard extends StatelessWidget {
const _SearchLiquidCard({
required this.child,
this.badgeCount,
this.trailing,
this.padding,
this.glowColor,
});
final IconData icon;
final Color accentColor;
final String title;
final Widget child;
final int? badgeCount;
final Widget? trailing;
final EdgeInsetsGeometry? padding;
final Color? glowColor;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final accent = glowColor ?? theme.colorScheme.primary;
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerLow,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withValues(alpha: 0.35)
: accent.withValues(alpha: 0.08),
blurRadius: 18,
offset: const Offset(0, 6),
spreadRadius: -2,
),
BoxShadow(
color: accent.withValues(alpha: isDark ? 0.06 : 0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 18, sigmaY: 18),
child: Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
theme.colorScheme.surfaceContainerHigh
.withValues(alpha: 0.60),
theme.colorScheme.surfaceContainerLow
.withValues(alpha: 0.38),
]
: [
Colors.white.withValues(alpha: 0.88),
Colors.white.withValues(alpha: 0.72),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.14)
: Colors.white.withValues(alpha: 0.85),
width: 1.2,
),
),
child: child,
),
),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 18, color: accentColor),
),
const SizedBox(width: 10),
Expanded(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
),
if (badgeCount != null && badgeCount! > 0) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$badgeCount',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
color: accentColor,
),
),
),
],
],
),
),
if (trailing != null) trailing!,
],
);
}
}
class _SearchIconBadge extends StatelessWidget {
const _SearchIconBadge({
required this.icon,
required this.color,
});
final IconData icon;
final Color color;
@override
Widget build(BuildContext context) {
const size = 34.0;
const iconSize = 18.0;
return Container(
width: size,
height: size,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color,
color.withValues(alpha: 0.86),
],
),
borderRadius: BorderRadius.circular(size * 0.28),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.35),
blurRadius: 7,
offset: const Offset(0, 2.5),
),
const SizedBox(height: 14),
child,
],
),
child: Center(
child: Icon(icon, size: iconSize, color: Colors.white),
),
);
}
}
@@ -465,19 +548,33 @@ class _QuickOperatorChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Material(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(12),
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
borderRadius: BorderRadius.circular(14),
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: color.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: color.withValues(alpha: 0.35),
color: color.withValues(alpha: isDark ? 0.40 : 0.32),
width: 1.1,
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: isDark ? 0.14 : 0.06),
blurRadius: 6,
offset: const Offset(0, 1.5),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -490,6 +587,7 @@ class _QuickOperatorChip extends StatelessWidget {
fontWeight: FontWeight.w700,
color: color,
fontFamily: 'monospace',
letterSpacing: -0.2,
),
),
],
@@ -516,44 +614,76 @@ class _CategoryTagRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
categoryName,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurfaceVariant,
),
Row(
children: [
Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.6),
blurRadius: 4,
),
],
),
),
const SizedBox(width: 8),
Text(
categoryName,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
color: theme.colorScheme.onSurface,
letterSpacing: -0.2,
),
),
],
),
const SizedBox(height: 8),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final tag in tags)
Material(
color: theme.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(12),
color: Colors.transparent,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => onTap(tag),
borderRadius: BorderRadius.circular(14),
onTap: () {
HapticFeedback.selectionClick();
onTap(tag);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
horizontal: 11, vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: isDark
? theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.45)
: Colors.white.withValues(alpha: 0.60),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: theme.colorScheme.outlineVariant
.withValues(alpha: 0.3),
color: isDark
? Colors.white.withValues(alpha: 0.10)
: theme.colorScheme.outlineVariant
.withValues(alpha: 0.35),
width: 1.0,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
width: 5,
height: 5,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
@@ -563,7 +693,8 @@ class _CategoryTagRow extends StatelessWidget {
Text(
tag,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
letterSpacing: -0.1,
),
),
],
@@ -587,14 +718,23 @@ class _TipRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
color: isDark
? theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.6)
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.8),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.08)
: theme.colorScheme.outlineVariant.withValues(alpha: 0.25),
),
),
child: Text(
code,
@@ -608,10 +748,14 @@ class _TipRow extends StatelessWidget {
),
const SizedBox(width: 10),
Expanded(
child: Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
height: 1.35,
),
),
),
),
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../backend/backend.dart';
@@ -95,20 +96,40 @@ class _RecentSearchesState extends State<RecentSearches> {
children: [
for (final item in visibleItems)
Material(
color: theme.colorScheme.surfaceContainer,
color: Colors.transparent,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => widget.onTap(item.query),
onTap: () {
HapticFeedback.selectionClick();
widget.onTap(item.query);
},
child: Container(
padding:
const EdgeInsets.only(left: 10, right: 6, top: 6, bottom: 6),
decoration: BoxDecoration(
color: Theme.of(context).brightness == Brightness.dark
? theme.colorScheme.surfaceContainerHighest
.withValues(alpha: 0.45)
: Colors.white.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.outlineVariant
.withValues(alpha: 0.35),
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.12)
: theme.colorScheme.outlineVariant
.withValues(alpha: 0.35),
width: 1.0,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(
alpha: Theme.of(context).brightness == Brightness.dark
? 0.15
: 0.04),
blurRadius: 6,
offset: const Offset(0, 1.5),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -140,7 +161,7 @@ class _RecentSearchesState extends State<RecentSearches> {
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.12),
color: theme.colorScheme.primary.withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(8),
),
child: Text(
@@ -161,7 +182,10 @@ class _RecentSearchesState extends State<RecentSearches> {
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => widget.onDelete!(item.id),
onTap: () {
HapticFeedback.lightImpact();
widget.onDelete!(item.id);
},
child: Padding(
padding: const EdgeInsets.all(4),
child: Icon(
@@ -23,6 +23,16 @@ class SettingsController extends AsyncNotifier<AppSettings> {
ref.invalidate(appSettingsProvider);
}
Future<void> setVideoPlayerVolume(double volume) async {
final current = state.value ?? AppSettings.defaults;
final clamped = volume.clamp(0.0, 100.0);
if ((current.videoPlayerVolume - clamped).abs() < 0.01) return;
final updated = current.copyWith(videoPlayerVolume: clamped);
state = AsyncData(updated);
await ref.read(settingsServiceProvider).setVideoPlayerVolume(clamped);
ref.invalidate(appSettingsProvider);
}
Future<void> clearCache() async {
await ref.read(cacheServiceProvider).clear();
}
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,8 @@
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@@ -156,28 +159,56 @@ class _ViewedScreenState extends ConsumerState<ViewedScreen> {
);
}
final bottomInset = MediaQuery.paddingOf(context).bottom;
return RefreshIndicator(
onRefresh: () =>
ref.read(viewedControllerProvider.notifier).refresh(),
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
padding: EdgeInsets.fromLTRB(16, 8, 16, 140 + bottomInset),
children: [
for (final group in groups) ...[
Padding(
padding: const EdgeInsets.fromLTRB(4, 16, 4, 10),
padding: const EdgeInsets.fromLTRB(2, 16, 2, 10),
child: Row(
children: [
Icon(
Icons.calendar_today_rounded,
size: 16,
color: Theme.of(context).colorScheme.primary,
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.primary.withValues(alpha: 0.82),
],
),
borderRadius: BorderRadius.circular(9),
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.32),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: const Center(
child: Icon(
Icons.calendar_today_rounded,
size: 14,
color: Colors.white,
),
),
),
const SizedBox(width: 8),
const SizedBox(width: 10),
Text(
_localizedGroupLabel(group.label, isRu),
style:
Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
),
),
const SizedBox(width: 8),
@@ -187,10 +218,24 @@ class _ViewedScreenState extends ConsumerState<ViewedScreen> {
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withValues(alpha: 0.5)
: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withValues(alpha: 0.08)
: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.20),
),
),
child: Text(
'${group.items.length}',
@@ -198,7 +243,10 @@ class _ViewedScreenState extends ConsumerState<ViewedScreen> {
.textTheme
.labelSmall
?.copyWith(
fontWeight: FontWeight.w600,
fontWeight: FontWeight.w700,
color: Theme.of(context).brightness == Brightness.dark
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.primary,
),
),
),
@@ -299,184 +347,251 @@ class _ViewedPostTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final isDark = Theme.of(context).brightness == Brightness.dark;
final image = MediaUrlSelector.preview(post).firstOrNull;
final timeStr =
'${viewedAt.hour.toString().padLeft(2, '0')}:${viewedAt.minute.toString().padLeft(2, '0')}';
return Card(
elevation: 0,
color: scheme.surfaceContainerLow,
margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: scheme.outlineVariant.withValues(alpha: 0.3),
),
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.28 : 0.05),
blurRadius: 14,
offset: const Offset(0, 4),
spreadRadius: -2,
),
],
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onOpen,
child: Padding(
padding: const EdgeInsets.all(10),
child: Row(
children: [
// Squircle Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: 68,
height: 68,
child: Stack(
fit: StackFit.expand,
children: [
image == null
? Container(
color: scheme.surfaceContainerHighest,
child: const Icon(Icons.image_not_supported_rounded),
)
: CachedNetworkImage(
imageUrl: image,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
color: scheme.surfaceContainerHighest,
),
errorWidget: (_, __, ___) => Container(
color: scheme.surfaceContainerHighest,
child: const Icon(
Icons.broken_image_rounded,
size: 20,
),
),
),
if (_isVideo)
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(4),
),
child: const Icon(
Icons.play_arrow_rounded,
size: 14,
color: Colors.white,
),
),
),
],
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
HapticFeedback.selectionClick();
onOpen();
},
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
scheme.surfaceContainerHigh
.withValues(alpha: 0.58),
scheme.surfaceContainerLow
.withValues(alpha: 0.36),
]
: [
Colors.white.withValues(alpha: 0.88),
Colors.white.withValues(alpha: 0.72),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.13)
: Colors.white.withValues(alpha: 0.85),
width: 1.1,
),
),
),
const SizedBox(width: 12),
// Post details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
child: Row(
children: [
Row(
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color:
scheme.primaryContainer.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(6),
),
child: Text(
post.providerName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
fontWeight: FontWeight.w600,
color: scheme.onPrimaryContainer,
// Squircle Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(13),
child: Container(
width: 68,
height: 68,
decoration: BoxDecoration(
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.10)
: scheme.outlineVariant.withValues(alpha: 0.30),
),
borderRadius: BorderRadius.circular(13),
),
child: Stack(
fit: StackFit.expand,
children: [
image == null
? Container(
color: scheme.surfaceContainerHighest,
child: const Icon(
Icons.image_not_supported_rounded),
)
: CachedNetworkImage(
imageUrl: image,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
color: scheme.surfaceContainerHighest,
),
errorWidget: (_, __, ___) => Container(
color: scheme.surfaceContainerHighest,
child: const Icon(
Icons.broken_image_rounded,
size: 20,
),
),
),
),
),
),
const SizedBox(width: 6),
Text(
timeStr,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: scheme.onSurfaceVariant,
if (_isVideo)
Positioned(
right: 4,
bottom: 4,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.65),
borderRadius: BorderRadius.circular(5),
),
child: const Icon(
Icons.play_arrow_rounded,
size: 14,
color: Colors.white,
),
),
),
],
),
],
),
),
const SizedBox(height: 4),
Text(
post.tags.isEmpty ? '#${post.id}' : post.tags.take(4).join(' '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
if (post.score != 0) ...[
const SizedBox(height: 2),
Row(
const SizedBox(width: 12),
// Post details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.thumb_up_rounded,
size: 12,
color: scheme.outline,
Row(
children: [
Flexible(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 7,
vertical: 2,
),
decoration: BoxDecoration(
color: scheme.primary
.withValues(alpha: isDark ? 0.20 : 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: scheme.primary.withValues(
alpha: isDark ? 0.30 : 0.20),
),
),
child: Text(
post.providerName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.primary,
),
),
),
),
const SizedBox(width: 6),
Text(
timeStr,
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: scheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(width: 4),
const SizedBox(height: 4),
Text(
'${post.score}',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: scheme.outline,
post.tags.isEmpty ? '#${post.id}' : post.tags.take(4).join(' '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.1,
),
),
if (post.score != 0) ...[
const SizedBox(height: 2),
Row(
children: [
Icon(
Icons.thumb_up_rounded,
size: 12,
color: scheme.outline,
),
const SizedBox(width: 4),
Text(
'${post.score}',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: scheme.outline,
fontWeight: FontWeight.w600,
),
),
],
),
],
],
),
],
),
// Action buttons
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 36, minHeight: 36),
tooltip: isFavorite
? (isRu ? 'В избранном' : 'In favorites')
: (isRu ? 'В избранное' : 'Add to favorites'),
onPressed: () {
HapticFeedback.selectionClick();
onFavorite();
},
icon: Icon(
isFavorite
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: isFavorite
? Colors.redAccent
: scheme.onSurfaceVariant,
),
),
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 36, minHeight: 36),
tooltip:
isRu ? 'Удалить из истории' : 'Delete from history',
onPressed: () {
HapticFeedback.lightImpact();
onDelete();
},
icon: Icon(
Icons.close_rounded,
size: 20,
color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],
),
),
// Action buttons
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
tooltip: isFavorite
? (isRu ? 'В избранном' : 'In favorites')
: (isRu ? 'В избранное' : 'Add to favorites'),
onPressed: onFavorite,
icon: Icon(
isFavorite
? Icons.favorite_rounded
: Icons.favorite_border_rounded,
color: isFavorite ? Colors.redAccent : scheme.onSurfaceVariant,
),
),
IconButton(
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
tooltip: isRu ? 'Удалить из истории' : 'Delete from history',
onPressed: onDelete,
icon: Icon(
Icons.close_rounded,
size: 20,
color: scheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
],
),
),
),
),
@@ -9,6 +9,7 @@ class AdaptiveScaffold extends StatelessWidget {
this.titleWidget,
this.actions = const [],
this.floatingActionButton,
this.resizeToAvoidBottomInset = false,
super.key,
});
@@ -17,11 +18,13 @@ class AdaptiveScaffold extends StatelessWidget {
final Widget body;
final List<Widget> actions;
final Widget? floatingActionButton;
final bool resizeToAvoidBottomInset;
@override
Widget build(BuildContext context) {
if (Responsive.isMobile(context)) {
return Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
appBar: AppBar(
title: titleWidget ?? Text(title),
actions: actions,
@@ -35,6 +38,7 @@ class AdaptiveScaffold extends StatelessWidget {
);
}
return Scaffold(
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
appBar: AppBar(title: titleWidget ?? Text(title), actions: actions),
body: body,
floatingActionButton: floatingActionButton,
+6 -4
View File
@@ -167,8 +167,10 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
void _scrollToEnd() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_tagScrollController.hasClients) {
final max = _tagScrollController.position.maxScrollExtent;
final target = (max - 40).clamp(0.0, max);
_tagScrollController.animateTo(
_tagScrollController.position.maxScrollExtent,
target,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
@@ -286,8 +288,8 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
const SizedBox(width: 6),
],
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 120,
constraints: BoxConstraints(
minWidth: _tags.isEmpty ? 120 : 64,
maxWidth: 300,
),
child: Focus(
@@ -567,7 +569,7 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
final draft = _controller.text.trim().toLowerCase();
if (draft.isEmpty) return '';
final raw = draft.split(RegExp(r'\s+')).last;
return raw.replaceAll(RegExp(r'^[\(\)]+|[\(\)]+$'), '').trim();
return raw.trim();
}
bool get _isEditing => _focusNode.hasFocus || _localDirty;
+258 -78
View File
@@ -10,7 +10,7 @@ import '../../app/responsive.dart';
import '../../backend/backend.dart';
import '../../core/utils/result.dart';
class AppShell extends ConsumerWidget {
class AppShell extends ConsumerStatefulWidget {
const AppShell({required this.child, super.key});
final Widget child;
@@ -33,69 +33,6 @@ class AppShell extends ConsumerWidget {
_Destination('settings', 'Settings', Icons.settings_rounded, '/settings'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings =
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
final artistConfigs =
ref.watch(_enabledArtistConfigsProvider).value ?? const [];
final destinations =
_visibleDestinations(settings, artistConfigs.isNotEmpty);
final ru = settings.languageCode == 'ru';
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
const _NavigateIntent('/search'),
LogicalKeySet(LogicalKeyboardKey.escape): const _BackIntent(),
LogicalKeySet(LogicalKeyboardKey.digit1): const _NavigateIntent('/'),
LogicalKeySet(LogicalKeyboardKey.digit2):
const _NavigateIntent('/search'),
LogicalKeySet(LogicalKeyboardKey.digit3):
const _NavigateIntent('/favorites'),
LogicalKeySet(LogicalKeyboardKey.digit4):
const _NavigateIntent('/viewed'),
LogicalKeySet(LogicalKeyboardKey.digit5):
const _NavigateIntent('/collections'),
LogicalKeySet(LogicalKeyboardKey.digit6):
const _NavigateIntent('/artists'),
LogicalKeySet(LogicalKeyboardKey.digit7):
const _NavigateIntent('/providers'),
LogicalKeySet(LogicalKeyboardKey.digit8):
const _NavigateIntent('/settings'),
},
child: Actions(
actions: {
_NavigateIntent: CallbackAction<_NavigateIntent>(
onInvoke: (intent) {
context.go(intent.location);
return null;
},
),
_BackIntent: CallbackAction<_BackIntent>(
onInvoke: (_) {
if (Navigator.of(context).canPop()) Navigator.of(context).pop();
return null;
},
),
},
child: Focus(
autofocus: true,
child: Responsive.isDesktop(context)
? _DesktopShell(
destinations: destinations,
ru: ru,
child: child,
)
: _MobileShell(
destinations: destinations,
ru: ru,
child: child,
),
),
),
);
}
static List<_Destination> _visibleDestinations(
AppSettings settings,
bool hasArtists,
@@ -106,6 +43,139 @@ class AppShell extends ConsumerWidget {
return true;
}).toList(growable: false);
}
@override
ConsumerState<AppShell> createState() => _AppShellState();
}
class _AppShellState extends ConsumerState<AppShell> {
final List<String> _tabHistory = [];
String? _currentPath;
bool _isBackNavigating = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final path = GoRouterState.of(context).uri.path;
if (_isBackNavigating) {
_isBackNavigating = false;
_currentPath = path;
return;
}
if (_currentPath != null && _currentPath != path) {
if (path == '/') {
_tabHistory.clear();
} else {
_tabHistory.remove(_currentPath);
_tabHistory.add(_currentPath!);
if (_tabHistory.length > 25) {
_tabHistory.removeAt(0);
}
}
}
_currentPath = path;
}
bool _handleBack() {
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
return true;
}
final currentPath = GoRouterState.of(context).uri.path;
while (_tabHistory.isNotEmpty && _tabHistory.last == currentPath) {
_tabHistory.removeLast();
}
if (_tabHistory.isNotEmpty) {
final previous = _tabHistory.removeLast();
_isBackNavigating = true;
context.go(previous);
return true;
}
if (currentPath != '/') {
_isBackNavigating = true;
context.go('/');
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
final settings =
ref.watch(appSettingsProvider).value ?? AppSettings.defaults;
final artistConfigs =
ref.watch(_enabledArtistConfigsProvider).value ?? const [];
final destinations =
AppShell._visibleDestinations(settings, artistConfigs.isNotEmpty);
final ru = settings.languageCode == 'ru';
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (didPop) return;
final handled = _handleBack();
if (!handled) {
SystemNavigator.pop();
}
},
child: Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyF):
const _NavigateIntent('/search'),
LogicalKeySet(LogicalKeyboardKey.escape): const _BackIntent(),
LogicalKeySet(LogicalKeyboardKey.digit1): const _NavigateIntent('/'),
LogicalKeySet(LogicalKeyboardKey.digit2):
const _NavigateIntent('/search'),
LogicalKeySet(LogicalKeyboardKey.digit3):
const _NavigateIntent('/favorites'),
LogicalKeySet(LogicalKeyboardKey.digit4):
const _NavigateIntent('/viewed'),
LogicalKeySet(LogicalKeyboardKey.digit5):
const _NavigateIntent('/collections'),
LogicalKeySet(LogicalKeyboardKey.digit6):
const _NavigateIntent('/artists'),
LogicalKeySet(LogicalKeyboardKey.digit7):
const _NavigateIntent('/providers'),
LogicalKeySet(LogicalKeyboardKey.digit8):
const _NavigateIntent('/settings'),
},
child: Actions(
actions: {
_NavigateIntent: CallbackAction<_NavigateIntent>(
onInvoke: (intent) {
context.go(intent.location);
return null;
},
),
_BackIntent: CallbackAction<_BackIntent>(
onInvoke: (_) {
_handleBack();
return null;
},
),
},
child: Focus(
autofocus: true,
child: Responsive.isDesktop(context)
? _DesktopShell(
destinations: destinations,
ru: ru,
child: widget.child,
)
: _MobileShell(
destinations: destinations,
ru: ru,
child: widget.child,
),
),
),
),
);
}
}
class _DesktopShell extends StatefulWidget {
@@ -282,36 +352,41 @@ class _MobileShell extends StatelessWidget {
? location == '/'
: location.startsWith(item.location),
);
final bottomInset = 76.0 + MediaQuery.paddingOf(context).bottom;
final isKeyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
final bottomInset = isKeyboardOpen
? 0.0
: 76.0 + MediaQuery.paddingOf(context).bottom;
return Scaffold(
extendBody: true,
body: MediaQuery(
data: MediaQuery.of(context).copyWith(
viewInsets: MediaQuery.of(context).viewInsets.copyWith(bottom: 0),
padding: MediaQuery.of(context).padding.copyWith(
bottom: bottomInset,
),
),
child: child,
),
floatingActionButton: location == '/settings'
floatingActionButton: (location == '/settings' || isKeyboardOpen)
? null
: Padding(
padding: const EdgeInsets.only(bottom: 64),
child: FloatingActionButton.small(
heroTag: 'mobile-settings',
tooltip: ru ? 'Настройки' : 'Settings',
onPressed: () => context.go('/settings'),
child: const Icon(Icons.settings_rounded),
padding: const EdgeInsets.only(bottom: 0, right: 2),
child: _LiquidGlassSettingsButton(
ru: ru,
onTap: () => context.go('/settings'),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
bottomNavigationBar: _LiquidGlassBottomBar(
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (index) => context.go(items[index].location),
items: items,
ru: ru,
),
bottomNavigationBar: isKeyboardOpen
? null
: _LiquidGlassBottomBar(
selectedIndex: selected < 0 ? 0 : selected,
onDestinationSelected: (index) =>
context.go(items[index].location),
items: items,
ru: ru,
),
);
}
}
@@ -571,6 +646,111 @@ class _LiquidNavItem extends StatelessWidget {
}
}
class _LiquidGlassSettingsButton extends StatefulWidget {
const _LiquidGlassSettingsButton({
required this.ru,
required this.onTap,
});
final bool ru;
final VoidCallback onTap;
@override
State<_LiquidGlassSettingsButton> createState() =>
_LiquidGlassSettingsButtonState();
}
class _LiquidGlassSettingsButtonState extends State<_LiquidGlassSettingsButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final scheme = theme.colorScheme;
return Tooltip(
message: widget.ru ? 'Настройки' : 'Settings',
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.mediumImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.88 : 1.0,
duration: const Duration(milliseconds: 160),
curve: Curves.easeOutCubic,
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.38 : 0.12),
blurRadius: 18,
offset: const Offset(0, 6),
spreadRadius: -1,
),
BoxShadow(
color: scheme.primary.withValues(alpha: isDark ? 0.20 : 0.12),
blurRadius: 12,
offset: const Offset(0, 2),
),
],
),
child: ClipOval(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
center: const Alignment(-0.3, -0.4),
radius: 1.1,
colors: isDark
? [
scheme.surfaceContainerHigh.withValues(alpha: 0.85),
scheme.surface.withValues(alpha: 0.75),
]
: [
Colors.white.withValues(alpha: 0.92),
Colors.white.withValues(alpha: 0.70),
],
),
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.24)
: Colors.white.withValues(alpha: 0.85),
width: 1.4,
),
),
child: Center(
child: AnimatedRotation(
turns: _pressed ? 0.125 : 0.0,
duration: const Duration(milliseconds: 250),
curve: Curves.easeOutBack,
child: Icon(
Icons.settings_rounded,
size: 23,
color: scheme.primary,
),
),
),
),
),
),
),
),
),
);
}
}
class _Destination {
const _Destination(
this.id,
+185 -22
View File
@@ -117,9 +117,14 @@ class _PostCardState extends ConsumerState<PostCard>
Widget build(BuildContext context) {
final post = _resolvedPost ?? widget.post;
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
? post.width / post.height
: _fallbackAspect(post.fileType);
: _fallbackAspect(isTextPost ? 'text' : post.fileType);
final mobile = MediaQuery.sizeOf(context).width < 700;
final urls = _feedUrls(post, mobile: mobile);
final imageUrl = urls.isEmpty ? post.previewUrl : urls.first;
@@ -202,6 +207,12 @@ class _PostCardState extends ConsumerState<PostCard>
enabled: widget.blurExplicit && sensitive,
child: LayoutBuilder(
builder: (context, constraints) {
if (isTextPost) {
return _TextPostCardPreview(
post: post,
hovered: _hovered && !mobile,
);
}
final dpr = MediaQuery.devicePixelRatioOf(context);
final cacheWidth =
(constraints.maxWidth * dpr).round().clamp(320, 1800);
@@ -239,27 +250,28 @@ class _PostCardState extends ConsumerState<PostCard>
),
),
),
// Bottom cinematic gradient overlay for badges and favorite
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 64,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.72),
Colors.transparent,
],
// Bottom cinematic gradient overlay for badges and favorite (only on media posts)
if (!isTextPost)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 64,
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black.withValues(alpha: 0.72),
Colors.transparent,
],
),
),
),
),
),
),
if (widget.showBadges) ...[
Positioned(
left: 8,
@@ -269,11 +281,14 @@ class _PostCardState extends ConsumerState<PostCard>
if (post.fileType.toLowerCase().contains('video') ||
post.fileType.toLowerCase().contains('webm') ||
post.fileType.toLowerCase().contains('mp4') ||
post.fileType.toLowerCase().contains('gif'))
post.fileType.toLowerCase().contains('gif') ||
isTextPost)
Positioned(
right: 8,
top: 8,
child: _MediaBadge(fileType: post.fileType),
child: _MediaBadge(
fileType: isTextPost ? 'text' : post.fileType,
),
),
Positioned(
left: 8,
@@ -434,6 +449,7 @@ class _PostCardState extends ConsumerState<PostCard>
double _fallbackAspect(String fileType) {
final normalized = fileType.toLowerCase();
if (normalized == 'text') return 0.95;
if (normalized.contains('video') || normalized.contains('webm')) {
return 16 / 9;
}
@@ -462,7 +478,7 @@ class _PostCardState extends ConsumerState<PostCard>
'${post.previewUrl} ${post.sampleUrl} ${post.fileUrl}'
.toLowerCase();
return {
'User-Agent': 'Lunaris/2.0.1 Flutter local booru browser',
'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
'Accept': '*/*',
if (lower.contains('gelbooru')) 'Referer': 'https://gelbooru.com/',
if (lower.contains('realbooru'))
@@ -596,6 +612,8 @@ class _FeedVideoPreviewState extends State<_FeedVideoPreview> {
controller: _controller,
fit: BoxFit.cover,
controls: null,
pauseUponEnteringBackgroundMode: false,
resumeUponEnteringForegroundMode: false,
),
),
),
@@ -773,7 +791,7 @@ class _MediaBadge extends StatelessWidget {
Icon(_icon(type), size: 13, color: Colors.white),
const SizedBox(width: 3.5),
Text(
type,
_label(type),
style: const TextStyle(
fontSize: 10.5,
color: Colors.white,
@@ -794,14 +812,159 @@ class _MediaBadge extends StatelessWidget {
return 'video';
}
if (value.contains('gif')) return 'gif';
if (value.contains('text')) return 'text';
return 'photo';
}
IconData _icon(String type) => switch (type) {
'video' => Icons.play_arrow_rounded,
'gif' => Icons.gif_box_rounded,
'text' => Icons.article_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 {
+10 -1
View File
@@ -28,6 +28,7 @@ class PostMasonryGrid extends StatelessWidget {
this.loading = false,
this.controller,
this.gridMode = 'masonry',
this.padding,
super.key,
});
@@ -51,11 +52,19 @@ class PostMasonryGrid extends StatelessWidget {
final bool loading;
final ScrollController? controller;
final String gridMode;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
final mobile = Responsive.isMobile(context);
final pad = EdgeInsets.all(mobile ? 8 : 16);
final bottomInset = MediaQuery.paddingOf(context).bottom;
final pad = padding ??
EdgeInsets.fromLTRB(
mobile ? 8 : 16,
mobile ? 8 : 16,
mobile ? 8 : 16,
(mobile ? 8 : 16) + (mobile ? (120.0 + bottomInset) : 0.0),
);
final spacing = mobile ? 8.0 : 12.0;
Widget buildCard(Post post) {
+78 -9
View File
@@ -3,22 +3,91 @@ import 'package:flutter/material.dart';
import '../../backend/backend.dart';
class ProviderStatusBadge extends StatelessWidget {
const ProviderStatusBadge({required this.health, super.key});
const ProviderStatusBadge({
required this.health,
this.compact = false,
super.key,
});
final ProviderHealth? health;
final bool compact;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final status = health?.status ?? ProviderStatus.unknown;
final color = switch (status) {
ProviderStatus.online => Colors.green,
ProviderStatus.offline => Colors.red,
ProviderStatus.unknown => Colors.grey,
final (color, labelRu, labelEn) = switch (status) {
ProviderStatus.online => (
const Color(0xFF10B981), // Emerald
'Онлайн',
'Online',
),
ProviderStatus.offline => (
const Color(0xFFEF4444), // Crimson Red
'Офлайн',
'Offline',
),
ProviderStatus.unknown => (
const Color(0xFF94A3B8), // Slate
'Не проверен',
'Unknown',
),
};
return Chip(
avatar: Icon(Icons.circle, color: color, size: 10),
label: Text(status.name),
visualDensity: VisualDensity.compact,
final isRu = Localizations.maybeLocaleOf(context)?.languageCode == 'ru';
final label = isRu ? labelRu : labelEn;
return Container(
padding: EdgeInsets.symmetric(
horizontal: compact ? 8 : 10,
vertical: compact ? 3 : 5,
),
decoration: BoxDecoration(
color: color.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: color.withValues(alpha: isDark ? 0.45 : 0.32),
width: 1.1,
),
boxShadow: [
BoxShadow(
color: color.withValues(alpha: isDark ? 0.18 : 0.08),
blurRadius: 6,
offset: const Offset(0, 1.5),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.8),
blurRadius: 5,
spreadRadius: 0.5,
),
],
),
),
const SizedBox(width: 6),
Text(
label,
style: theme.textTheme.labelSmall?.copyWith(
color: color,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
],
),
);
}
}
+103 -11
View File
@@ -1,36 +1,128 @@
import 'package:flutter/material.dart';
import '../../backend/models/tag_suggestion.dart';
class TagChip extends StatelessWidget {
const TagChip({
required this.tag,
this.category,
this.group,
this.onTap,
this.onLongPress,
super.key,
});
final String tag;
final TagCategory? category;
final String? group;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
TagCategory get _resolvedCategory {
if (category != null) return category!;
final g = (group ?? '').toLowerCase();
return switch (g) {
'artist' => TagCategory.artist,
'character' => TagCategory.character,
'copyright' => TagCategory.copyright,
'species' => TagCategory.species,
'meta' => TagCategory.meta,
'general' => TagCategory.general,
_ => tagCategoryFromString(g),
};
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final chip = ActionChip(
onPressed: onTap,
label: Text(tag),
labelStyle: TextStyle(
color: scheme.onSecondaryContainer,
fontWeight: FontWeight.w700,
final isDark = Theme.of(context).brightness == Brightness.dark;
final cat = _resolvedCategory;
final (Color baseColor, Color textColor) = _colorsForCategory(cat, scheme, isDark);
final chipWidget = Container(
decoration: BoxDecoration(
color: baseColor.withValues(alpha: isDark ? 0.16 : 0.10),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: baseColor.withValues(alpha: isDark ? 0.40 : 0.28),
width: 1.0,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
color: baseColor,
shape: BoxShape.circle,
),
),
Text(
tag,
style: TextStyle(
color: textColor,
fontWeight: FontWeight.w700,
fontSize: 13,
letterSpacing: -0.1,
),
),
],
),
),
),
),
backgroundColor: scheme.secondaryContainer.withValues(alpha: 0.62),
side: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.65)),
visualDensity: VisualDensity.compact,
);
if (onLongPress == null) return chip;
if (onLongPress == null) return chipWidget;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: onLongPress,
child: chip,
child: chipWidget,
);
}
static (Color, Color) _colorsForCategory(
TagCategory cat,
ColorScheme scheme,
bool isDark,
) {
return switch (cat) {
TagCategory.artist => (
const Color(0xFFFF4757), // e621 / Gelbooru red/coral
isDark ? const Color(0xFFFF6B81) : const Color(0xFFD63031),
),
TagCategory.character => (
const Color(0xFF2ED573), // Vibrant character green
isDark ? const Color(0xFF7BED9F) : const Color(0xFF10AC84),
),
TagCategory.copyright => (
const Color(0xFFA55EEA), // Classic anime series purple
isDark ? const Color(0xFFD980FA) : const Color(0xFF8854D0),
),
TagCategory.species => (
const Color(0xFFFFA502), // e621 species amber/orange
isDark ? const Color(0xFFFFC048) : const Color(0xFFE67E22),
),
TagCategory.meta => (
const Color(0xFF1E90FF), // Meta / format cyan / ice blue
isDark ? const Color(0xFF70A1FF) : const Color(0xFF0984E3),
),
TagCategory.general || TagCategory.unknown => (
isDark ? scheme.primary : scheme.primary,
isDark ? scheme.onSurface : scheme.onSurface,
),
};
}
}
+5 -2
View File
@@ -1,6 +1,8 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
project(runner LANGUAGES CXX C)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=incompatible-pointer-types -Wno-incompatible-pointer-types")
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
@@ -41,7 +43,8 @@ endif()
# of modifying this function.
function(APPLY_STANDARD_SETTINGS TARGET)
target_compile_features(${TARGET} PUBLIC cxx_std_14)
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
target_compile_options(${TARGET} PRIVATE -Wall)
target_compile_options(${TARGET} PRIVATE "$<$<COMPILE_LANGUAGE:C>:-Wno-error=incompatible-pointer-types;-Wno-incompatible-pointer-types>")
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
endfunction()
+2 -2
View File
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
if (use_header_bar) {
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
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_window_set_titlebar(window, GTK_WIDGET(header_bar));
} else {
gtk_window_set_title(window, "Lunaris");
gtk_window_set_title(window, "Prisma");
}
gtk_window_set_default_size(window, 1280, 720);
+7
View File
@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "ephemeral/Flutter-Generated.xcconfig"
@@ -0,0 +1,34 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import connectivity_plus
import dynamic_color
import file_selector_macos
import isar_flutter_libs
import media_kit_libs_macos_video
import media_kit_video
import package_info_plus
import share_plus
import sqflite_darwin
import url_launcher_macos
import volume_controller
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
+46
View File
@@ -0,0 +1,46 @@
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15'
end
end
end
+729
View File
@@ -0,0 +1,729 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
33CC10ED2044A3C60003C045 /* gel_rule_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "gel_rule_app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* gel_rule_app.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* gel_rule_app.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.rardog.gelRuleApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gel_rule_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gel_rule_app";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.rardog.gelRuleApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gel_rule_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gel_rule_app";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.rardog.gelRuleApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/gel_rule_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/gel_rule_app";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "gel_rule_app.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "gel_rule_app.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "gel_rule_app.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "gel_rule_app.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "gel_rule_app.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
return true
}
}
@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+343
View File
@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>
+14
View File
@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = Prisma
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.rardog.prisma
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 com.rardog. All rights reserved.
+2
View File
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"
+13
View File
@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
+15
View File
@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+12
View File
@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+130 -114
View File
@@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
url: "https://pub.dev"
source: hosted
version: "4.0.9"
version: "4.2.0"
args:
dependency: transitive
description:
@@ -69,10 +69,10 @@ packages:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
sha256: e1d40ef3f7934986d5da2271b1ba07794921ce263e44d622fb6c406d76589e33
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.6"
build_resolvers:
dependency: transitive
description:
@@ -109,10 +109,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7
url: "https://pub.dev"
source: hosted
version: "8.12.6"
version: "8.13.0"
cached_network_image:
dependency: "direct main"
description:
@@ -157,26 +157,26 @@ packages:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: "67cf6d84013f9c601e42a6f8a6b74c4c0d9dc1a1619d775f2b28b732d3551b85"
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
sha256: aa5932e94c6c39c2f9ec4e5e06dfdd11a9430a61f6c41b6ba75b28ce0c481baf
url: "https://pub.dev"
source: hosted
version: "4.11.1"
version: "4.12.0"
collection:
dependency: transitive
description:
@@ -213,10 +213,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
version: "0.3.5+5"
crypto:
dependency: transitive
description:
@@ -253,26 +253,26 @@ packages:
dependency: transitive
description:
name: dbus
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383
url: "https://pub.dev"
source: hosted
version: "0.7.12"
version: "0.7.15"
dio:
dependency: "direct main"
description:
name: dio
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"
url: "https://pub.dev"
source: hosted
version: "5.9.2"
version: "5.11.1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.2.2"
dynamic_color:
dependency: "direct main"
description:
@@ -317,34 +317,34 @@ packages:
dependency: transitive
description:
name: file_selector_android
sha256: "89243030ea4b3463fb402b44d5eeacc4ccb1c46a88870cb2a5080d693200c1ed"
sha256: "7c76473740e33a11343c8fce88166049230850d2f19cd8a652ea935fcb8c9206"
url: "https://pub.dev"
source: hosted
version: "0.5.2+6"
version: "0.5.2+10"
file_selector_ios:
dependency: transitive
description:
name: file_selector_ios
sha256: e2ecf2885c121691ce13b60db3508f53c01f869fb6e8dc5c1cfa771e4c46aeca
sha256: "97269e5307a0ab813b1fa2430bada0a96e0afb74848417f8676f64ba5de0051c"
url: "https://pub.dev"
source: hosted
version: "0.5.3+5"
version: "0.5.3+6"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
sha256: da76400e7872ce7637ffdce12749ec24169c25f6195c28372208e65a24bcd2ab
url: "https://pub.dev"
source: hosted
version: "0.9.4"
version: "0.9.4+1"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
sha256: d57c62362766b5e7ae739448650b66c6aab7a68ba7ecc65e04018652645ae0f4
url: "https://pub.dev"
source: hosted
version: "0.9.5"
version: "0.9.5+1"
file_selector_platform_interface:
dependency: transitive
description:
@@ -357,18 +357,18 @@ packages:
dependency: transitive
description:
name: file_selector_web
sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7
sha256: "73181fbc5257776d8ecaa6a94ab3c8e920ad143b9132a6d984a9271dfc6928d3"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
version: "0.9.5"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
sha256: fbefc5fb92c6d3cbe8d284a2cd971b593bb07d2cd6da8557b81a862250b4acec
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
version: "0.9.3+6"
fixnum:
dependency: transitive
description:
@@ -386,10 +386,10 @@ packages:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
sha256: "1de7849213b4c73c85aca7e0ac687a9a5d82ccdb594366b9dcc26cb6a2189cd2"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
version: "3.4.2"
flutter_lints:
dependency: "direct dev"
description:
@@ -452,10 +452,10 @@ packages:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.2.0"
go_router:
dependency: "direct main"
description:
@@ -476,10 +476,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: a41af4e8fc687cd6d33de9751eb936c8c0204ebe2bcb6c15ecf707504bf47f31
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.0.2"
http:
dependency: transitive
description:
@@ -508,18 +508,18 @@ packages:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
sha256: "1976370a4df3091bb0f72409c187ad1f9132a818bc6b95ca59c0bae1c75c688e"
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.9.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f"
url: "https://pub.dev"
source: hosted
version: "1.0.5"
version: "1.1.0"
isar:
dependency: "direct main"
description:
@@ -547,18 +547,26 @@ packages:
dependency: transitive
description:
name: jni
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.0.3"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.3"
jni_util:
dependency: transitive
description:
name: jni_util
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
js:
dependency: transitive
description:
@@ -663,6 +671,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.1"
media_kit_libs_macos_video:
dependency: "direct main"
description:
name: media_kit_libs_macos_video
sha256: f26aa1452b665df288e360393758f84b911f70ffb3878032e1aabba23aa1032d
url: "https://pub.dev"
source: hosted
version: "1.1.4"
media_kit_libs_windows_video:
dependency: "direct main"
description:
@@ -683,18 +699,18 @@ packages:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.18.3"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "2.1.0"
mocktail:
dependency: "direct dev"
description:
@@ -715,10 +731,10 @@ packages:
dependency: transitive
description:
name: objective_c
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
url: "https://pub.dev"
source: hosted
version: "9.4.1"
version: "9.5.0"
octo_image:
dependency: transitive
description:
@@ -763,10 +779,10 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
path_provider_android:
dependency: transitive
description:
@@ -787,18 +803,18 @@ packages:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.3"
path_provider_windows:
dependency: transitive
description:
@@ -835,34 +851,34 @@ packages:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
version: "1.5.3"
posix:
dependency: transitive
description:
name: posix
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.0"
version: "6.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
sha256: c38b81cbf34450b67e0265d73433569d12e34782e30ed769c9cc99c9d5f2e796
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "1.6.0"
record_use:
dependency: transitive
description:
@@ -891,26 +907,26 @@ packages:
dependency: transitive
description:
name: safe_local_storage
sha256: "287ea1f667c0b93cdc127dccc707158e2d81ee59fba0459c31a0c7da4d09c755"
sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.0.6"
screen_brightness_android:
dependency: transitive
description:
name: screen_brightness_android
sha256: "2df5e0488a22a73271a43d92dbcd80643df6910cf69690faacaf9929ae9ade3b"
sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.1.6"
screen_brightness_platform_interface:
dependency: transitive
description:
name: screen_brightness_platform_interface
sha256: "59d50850d6735d677780fc7359c8e997d0ff6df91c8465161c9e617a7b0a11d8"
sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
share_plus:
dependency: "direct main"
description:
@@ -984,50 +1000,50 @@ packages:
dependency: transitive
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
sha256: "58a799e6ac17dd32fbab93813d39ed835a75ccc0f8f85b8955fe318c6712b082"
url: "https://pub.dev"
source: hosted
version: "2.4.2+1"
version: "2.4.3"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
sha256: d0548f9d7422a2dae99ec6f8b0a3074463b132d216fa5ba0d230eeefc901983b
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
version: "2.4.3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
sha256: "5bf6a55c166e73bf651ba7ec3ed486e577620e3dc8f3a9c6a258a8031b624590"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
version: "2.5.11"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
sha256: c86ca18b8f666bbf903924687fe21cc16fc385d086005067e26619ca530bef9f
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.3+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
sha256: f84939f84350d92d04416f8bc4dc52d3896aec7716cc9e80cf0146342139dc50
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
version: "1.12.2"
state_notifier:
dependency: transitive
description:
@@ -1048,10 +1064,10 @@ packages:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a
url: "https://pub.dev"
source: hosted
version: "2.1.1"
version: "2.1.2"
string_scanner:
dependency: transitive
description:
@@ -1064,10 +1080,10 @@ packages:
dependency: transitive
description:
name: synchronized
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
sha256: "3a7b5d17422dd0f8d5c6c14feaa5a1c65638b9455f871a96f08437562c046931"
url: "https://pub.dev"
source: hosted
version: "3.4.0+1"
version: "3.4.1+2"
term_glyph:
dependency: transitive
description:
@@ -1136,34 +1152,34 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
url: "https://pub.dev"
source: hosted
version: "6.3.30"
version: "6.3.33"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
version: "6.4.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
version: "3.2.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
version: "3.2.6"
url_launcher_platform_interface:
dependency: transitive
description:
@@ -1184,42 +1200,42 @@ packages:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
version: "3.1.6"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
version: "4.6.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
version: "15.3.0"
volume_controller:
dependency: transitive
description:
name: volume_controller
sha256: bed7c57b1da19d60a58e2ed598b6824754cd9af1b728b26fb966a8745c72629c
sha256: "9e77687434a3f4095d8faf5321ed1c8014f78d5c555231543717c155f03b446f"
url: "https://pub.dev"
source: hosted
version: "3.5.0"
version: "3.6.1"
wakelock_plus:
dependency: transitive
description:
@@ -1232,10 +1248,10 @@ packages:
dependency: transitive
description:
name: wakelock_plus_platform_interface
sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b
sha256: "764c25504562abc8ac3406f5d175f126b276e6ba916c7c2675690b73368ca384"
url: "https://pub.dev"
source: hosted
version: "1.5.1"
version: "1.7.0"
watcher:
dependency: transitive
description:
@@ -1288,10 +1304,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
xxh3:
dependency: transitive
description:
@@ -1304,10 +1320,10 @@ packages:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4"
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
+2 -2
View File
@@ -1,7 +1,7 @@
name: gel_rule_app
description: Local backend/core layer for a Flutter booru browser app.
publish_to: "none"
version: 3.0.7+58
version: 3.5.2+78
environment:
sdk: ">=3.3.0 <4.0.0"
@@ -26,6 +26,7 @@ dependencies:
media_kit_video: ^1.2.4
media_kit_libs_windows_video: ^1.0.10
media_kit_libs_linux: ^1.1.3
media_kit_libs_macos_video: ^1.1.0
shimmer: ^3.0.0
url_launcher: ^6.3.0
file_selector: ^1.0.3
@@ -33,7 +34,6 @@ dependencies:
media_kit_libs_android_video: ^1.3.8
share_plus: ^10.0.0
dynamic_color: ^1.7.0
dev_dependencies:
flutter_test:
sdk: flutter
+1 -1
View File
@@ -10,7 +10,7 @@ void main() {
late TagCacheService cacheService;
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';
cacheService = TagCacheService(customPath: cacheFilePath);
await cacheService.init();
+8 -8
View File
@@ -13,24 +13,24 @@ void main() {
final info = AppUpdateInfo(
version: '2.0.2',
tagName: 'v2.0.2',
name: 'Lunaris 2.0.2',
name: 'Prisma 2.0.2',
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'),
apkUrl: 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/Lunaris.apk',
linuxTarGzUrl: 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/linux.tar.gz',
apkUrl: 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/Prisma.apk',
linuxTarGzUrl: 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/linux.tar.gz',
);
expect(info.hasAssets, isTrue);
expect(info.apkUrl, contains('apk'));
expect(info.linuxTarGzUrl, contains('linux'));
expect(
service.assetFileName(info, 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/Lunaris-v2.0.2.apk'),
'Lunaris-v2.0.2.apk',
service.assetFileName(info, 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/Prisma-v2.0.2.apk'),
'Prisma-v2.0.2.apk',
);
expect(
service.assetFileName(info, 'https://github.com/RarDog/Lunaris/releases/download/v2.0.2/unknown-linux'),
'Lunaris-v2.0.2-linux-x64.tar.gz',
service.assetFileName(info, 'https://github.com/RarDog/Prisma/releases/download/v2.0.2/unknown-linux'),
'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 Stream'), findsOneWidget);
// Verify commentary
expect(find.text('Описание от автора'), findsOneWidget);
expect(find.text('Special Patreon bonus animation for all patrons.'), findsOneWidget);
// Verify play button triggers callback
// Verify play button triggers callback on mirrors tab
final playBtn = find.text('Смотреть');
expect(playBtn, findsOneWidget);
await tester.tap(playBtn);
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 {

Some files were not shown because too many files have changed in this diff Show More