Compare commits

...
16 Commits
Author SHA1 Message Date
RarDog af00cb8179 fix: resolve video misclassification when filename contains 'audio' and remove legacy Kemono/Coomer providers
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 15:35:41 +03:00
RarDog 5e3efb0da9 feat: add audio playback support (MP3/WAV/FLAC) and fix attachment loading in creator posts
Build and Release / Build Windows x64 (push) Has been cancelled
Build and Release / Build macOS (push) Has been cancelled
2026-09-05 15:16:08 +03:00
RarDog 2dad5e9c82 docs: adjust vibe coding disclaimer wording 2026-09-05 14:42:47 +03:00
RarDog aba8ceb13c docs: add Vibe Coding disclaimer to English and Russian READMEs 2026-09-05 14:40:46 +03:00
RarDog 5fba08cd01 docs: add English README and preserve Russian README_RU with language switcher 2026-09-05 14:22:58 +03:00
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
57 changed files with 4323 additions and 1448 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.
+96 -76
View File
@@ -1,3 +1,7 @@
<p align="center">
<strong>English</strong> • <a href="README_RU.md">Русский</a>
</p>
# 💎 Prisma
<p align="center">
@@ -5,124 +9,127 @@
</p>
<p align="center">
<strong>Современный кроссплатформенный клиент для Booru и Creator-архивов нового поколения</strong><br>
Плавный Pinterest-стиль, интеграция Pawchive 2.0, двусторонняя синхронизация, интеллектуальное автообновление и воспроизведение медиа без задержек.
<strong>A modern, next-generation cross-platform client for Booru imageboards and Creator archives</strong><br>
Smooth Pinterest-style masonry feed, Pawchive 2.0 two-way synchronization, intelligent in-app updates, dynamic aspect ratio detection, and buttery-smooth media playback.
</p>
<p align="center">
<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>
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.4-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.4" /></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" />
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
<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>
---
## 🚀 Что нового в Prisma 3.0 (Major Release)
## 🚀 What's New in Prisma 3.5
### 🎨 Дизайн 2.0
- **Современный визуальный стиль**: Полный переход на squircle-карточки, адаптивную цветовую палитру и улучшенную Material You динамическую тему.
- **Интеллектуальный Action Dock 2.0**: Компактный док ключевых действий под контентом с быстрым доступом к скачиванию, шерингу, тегам и инфо.
- **Плавные анимации и микро-взаимодействия**: Открытие медиа с эффектом Hero, интерактивное листание свайпами и анимированный лайк по двойному тапу.
### 🎨 Design 2.0 & Interface Optimization
- **Modern Visual Style**: Squircle cards, adaptive dynamic Material You color palette, and refined glassmorphic surfaces.
- **Intelligent Action Dock 2.0**: Compact quick-action dock placed beneath posts for one-tap downloading, sharing, tag inspection, and source viewing.
- **Smooth Animations & Micro-Interactions**: Hero media transitions, interactive drag gestures, and animated double-tap likes.
- **Keyboard-Adaptive Layout**: Floating action buttons and navigation dock automatically minimize when the on-screen keyboard appears; search lists support dismiss-on-scroll.
### 🐾 Глубокая интеграция Pawchive 2.0
- **Мульти-аккаунты**: Поддержка авторизации по логину и паролю, а также прямому вводу `Session Cookie`.
- **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Prisma и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап.
- **Моментальная синхронизация**: При нажатии кнопки «В избранное» у любого автора (Fanbox, Patreon, Fantia, Boosty, Gumroad, DLsite, Discord) статус синхронизируется с сервером на лету.
### 🐾 Deep Pawchive 2.0 Integration
- **Flexible Authentication**: Sign in via username/password or direct `Session Cookie`.
- **Two-Way Synchronization**: Automatically synchronize your favorites between Prisma's local database and your Pawchive account. Local creators can be pushed to the server with a single tap.
- **Instant Cloud Sync**: Favoriting any creator across Patreon, Fanbox, Fantia, Boosty, Gumroad, DLsite, or Discord synchronizes with the server in real-time.
### 🎬 Умный медиаплеер и определение разрешения
- **Ленивое вычисление габаритов (1920×1080 и др.)**: Определение точного разрешения картинок и видео без полной предварительной загрузки тяжелых файлов.
- **Автоповорот в полноэкранном режиме**: Видеоплеер автоматически ориентирует экран (альбомный или портретный режим) в зависимости от аспектного соотношения медиа.
### 🎬 Smart Media Player & Dimension Detection
- **Lazy Dimension Resolution**: Instant 1920×1080 (and custom) aspect-ratio discovery without needing to pre-download large media files.
- **Fullscreen Auto-Orientation**: Video player automatically adjusts screen orientation (landscape or portrait) to match the media's native aspect ratio.
### 📦 Разделение APK и умное автообновление по ABI
- Сборка облегчённых APK с разделением по архитектурам процессора (`arm64-v8a`, `armeabi-v7a`, `x86_64`).
- Встроенная система обновлений автоматически определяет архитектуру вашего Android-устройства и скачивает только нужный APK, экономя трафик и ускоряя загрузку.
### 📦 Split APKs & ABI-Aware Auto-Updates
- Optimized per-ABI APK builds (`arm64-v8a`, `armeabi-v7a`, `x86_64`) for drastically reduced package sizes.
- Built-in update engine automatically detects the device CPU architecture and fetches only the required binary.
---
## 📚 Документация и Вики (Руководство пользователя)
## 📚 Documentation & User Guides
Подробные иллюстрированные руководства доступны в **[Prisma Wiki](https://github.com/RarDog/Prisma/wiki)** и прямо в репозитории:
Comprehensive illustrated documentation is available in the **[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.
- 🔍 **[Умный поиск и теги](docs/wiki/Search-and-Tags.md)** — оператор `and`, общие модификаторы и подсказки за 0 мс.
- 🐾 **[Интеграция с Pawchive](docs/wiki/Pawchive-Sync.md)** — вход по паролю/куки, двусторонняя синхронизация и выгрузка авторов.
- 🎬 **[Медиаплеер и просмотр](docs/wiki/Media-Viewer.md)** — жесты, автоповорот видео, разрешение 1920×1080 и Action Dock 2.0.
- 💾 **[Офлайн-режим и загрузки](docs/wiki/Offline-and-Downloads.md)** — Sync Hub, шаблоны папок и сохранение оригиналов.
- 🛡️ **[Черный список и фильтрация](docs/wiki/Safety-and-Blacklist.md)** — настройка Blacklist, Safe Mode и очистка кэша.
- ❓ **[Часто задаваемые вопросы (FAQ)](docs/wiki/FAQ.md)** — автообновление, звук в видео и решение проблем.
- 🚀 **[Getting Started & Installation](docs/wiki/Getting-Started.md)** — Selecting the proper APK for your device architecture, running on Linux, Windows, or macOS.
- 🌐 **[Content Providers](docs/wiki/Providers.md)** — Using Booru sources (Gelbooru, Rule34, Realbooru, e621) and Pawchive creator archives.
- 🔍 **[Smart Search & Tags](docs/wiki/Search-and-Tags.md)** — `and` operators, syntax modifiers, and 0 ms autocomplete.
- 🐾 **[Pawchive Integration](docs/wiki/Pawchive-Sync.md)** — Login, session cookies, two-way sync, and creator cloud exports.
- 🎬 **[Media Viewer & Player](docs/wiki/Media-Viewer.md)** — Gesture navigation, automatic rotation, and Action Dock 2.0.
- 💾 **[Offline Mode & Downloads](docs/wiki/Offline-and-Downloads.md)** — Sync Hub, directory naming templates, and original quality preservation.
- 🛡️ **[Safety & Blacklist](docs/wiki/Safety-and-Blacklist.md)** — Custom blacklist rules, Safe Mode, and cache management.
- ❓ **[Frequently Asked Questions (FAQ)](docs/wiki/FAQ.md)** — Troubleshooting, updater, audio playback, and common solutions.
---
## ✨ Ключевые возможности
## ✨ Key Features
- **📱 Плавная лента в стиле Pinterest**: Masonry-сетка с переключением режима отображения (Masonry, 1:1 Сетка, Компактный список).
- **💾 Офлайн 2.0 и Sync Hub**: Просмотр фото и видео прямо из локального хранилища без запросов в сеть, аналитика занятого места и пакетная выгрузка.
- **🔍 Поиск 2.0 с операторами**: Быстрые подсказки тегов со скоростью отклика 0 мс, оператор `and` для мульти-поиска, фильтры `type:video`, `rating:safe`.
- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация нежелательного контента по тегам, категориям, рейтингу и минимальному score.
- **📁 Умный менеджер загрузок**: Шаблоны поддиректорий (`{Artist}`, `{Provider}`, `{Service}`, `{ID}`, `{Date}`) для аккуратного архива.
- **🌑 AMOLED Pure Black**: Истинно черный фон для максимальной энергоэффективности на OLED/AMOLED дисплеях.
- **📱 Pinterest-Style Masonry Feed**: Staggered grid layout with instant mode switching (Masonry, 1:1 Square Grid, Compact List).
- **💾 Offline 2.0 & Sync Hub**: Browse saved media directly from local storage with zero network requests, storage analytics, and batch export.
- **🔍 Search 2.0**: Instant tag autocomplete, multi-tag queries with `and`, and modifiers such as `type:video` or `rating:safe`.
- **🛡️ Advanced Blacklist / Whitelist**: Filter undesirable content by tags, categories, age ratings, or minimum score thresholds.
- **📁 Smart Download Manager**: Flexible directory and filename templates (`{Artist}`, `{Provider}`, `{Service}`, `{ID}`, `{Date}`).
- **🌑 AMOLED Pure Black**: True deep black background optimized for OLED/AMOLED display power efficiency.
---
## 🌐 Поддерживаемые провайдеры
## 🌐 Supported Providers
| Провайдер | Тип API | Поддерживаемый контент |
| Provider | API Type | Supported Content |
|---|---|---|
| **Pawchive** | REST API v1 | Patreon, Pixiv Fanbox, Fantia, Boosty, Gumroad, DLsite, Discord |
| **Gelbooru** | JSON / XML API | Аниме-арты, теги, авторы, рейтинги |
| **Rule34** | Booru API | Огромная база booru-контента, видео и GIF |
| **Realbooru** | Scraper / API | Фотосессии и реалистичный контент |
| **e621 / e926** | REST JSON API | Фурри- и антро-арт с богатейшей таксономией тегов |
| **Gelbooru** | JSON / XML API | Anime artworks, tag taxonomy, creator profiles, ratings |
| **Rule34** | Booru API | Expansive booru database, animated WebM/MP4 and GIF |
| **Realbooru** | Scraper / API | Cosplay photography and realistic media |
| **e621 / e926** | REST JSON API | Furry and anthropomorphic art with granular tag search |
---
## 📥 Загрузка (Скачать сборки)
## 📥 Downloads & Releases
Релизы доступны на странице **[GitHub Releases](https://github.com/RarDog/Prisma/releases)**.
Pre-compiled production releases are available on the **[GitHub Releases](https://github.com/RarDog/Prisma/releases)** page.
| Платформа | Архитектура | Формат | Описание |
| Platform | Architecture | Format | Notes |
|---|---|---|---|
| **Android** | `arm64-v8a` | `.apk` | Оптимизировано для современных смартфонов (64-bit) |
| **Android** | `armeabi-v7a` | `.apk` | Для более старых Android-устройств (32-bit) |
| **Android** | `x86_64` | `.apk` | Для эмуляторов Android и x86-планшетов |
| **Linux** | `x86_64` | `.tar.gz` | Портативный архив для Linux-дистрибутивов |
| **Windows** | `x64` | `.zip` / `.exe` | Портативная и установочная сборка для Windows 10/11 |
| **Android** | `arm64-v8a` | `.apk` | Recommended for all modern 64-bit Android smartphones |
| **Android** | `armeabi-v7a` | `.apk` | Legacy 32-bit Android devices |
| **Android** | `x86_64` | `.apk` | Android emulators, Chromebooks & x86 tablets |
| **Linux** | `x86_64` | `.tar.gz` | Portable standalone package for Linux distributions |
| **Windows** | `x64` | `.zip` | Portable release for Windows 10 & 11 |
| **macOS** | `Universal` | `.zip` | Portable build for macOS |
---
## 🛠️ Сборка из исходников
## 🛠️ Building from Source
### Требования
### Prerequisites
- **Flutter SDK**: `>= 3.22.0` (Dart `>= 3.4.0`)
- **Android SDK**: Build-Tools & Platform-Tools (для Android)
- **Android SDK**: Build-Tools & Platform-Tools (for Android targets)
- **Linux**: `clang`, `cmake`, `ninja-build`, `pkg-config`, `libgtk-3-dev`
- **Windows**: Visual Studio 2022 с компонентом «Desktop development with C++»
- **Windows**: Visual Studio 2022 with "Desktop development with C++"
- **macOS**: Xcode 15+
### Быстрый старт
### Quick Start
```bash
# Клонирование репозитория
# Clone repository
git clone https://github.com/RarDog/Prisma.git
cd Prisma
# Установка зависимостей
# Fetch dependencies
flutter pub get
# Проверка анализатора и запуск тестов
# Run static analysis and tests
flutter analyze
flutter test
# Запуск в режиме разработки
# Launch in debug mode
flutter run
```
### Сборка релизных пакетов
### Building Release Packages
```bash
# Android (раздельные APK по архитектурам)
# Android (Split APKs by ABI)
flutter build apk --release --split-per-abi
# Linux (Desktop bundle)
@@ -130,43 +137,56 @@ flutter build linux --release
# Windows (Desktop bundle)
flutter build windows --release
# macOS (Desktop bundle)
flutter build macos --release
```
---
## 🏛️ Архитектура проекта
## 🏛️ Project Architecture
```text
lib/
├── app/ # Роутер (GoRouter), темы, локализация (RU/EN), анимации
├── app/ # Routing (GoRouter), theme configuration, l10n (EN/RU), animations
├── backend/
│ ├── di/ # Внедрение зависимостей (Riverpod providers)
│ ├── di/ # Riverpod dependency injection & providers
│ ├── models/ # Post, ArtistProfile, PawchiveAccount, AppSettings
│ ├── providers/ # Клиенты API (Gelbooru, Rule34, Pawchive, e621)
│ ├── repositories/ # Локальная база данных Isar и кэширование
│ └── services/ # Сервисы: PawchiveSync, Feed, Search, Downloads, Updates
├── core/ # Сетевой клиент (Dio), база данных, утилиты
├── features/ # Модули экранов: feed, post, artists, favorites, settings
└── shared/widgets/ # Переиспользуемые компоненты (PostCard, MasonryGrid, Dock)
│ ├── providers/ # API integrations (Gelbooru, Rule34, Pawchive, e621)
│ ├── repositories/ # Local Isar database storage & cache layer
│ └── services/ # Core services: PawchiveSync, Feed, Search, Downloads, Updates
├── core/ # Network client (Dio), database engine, utilities
├── features/ # Feature screens: feed, post, artists, favorites, settings
└── shared/widgets/ # Reusable UI components (PostCard, MasonryGrid, Dock)
```
---
## 🔒 Безопасность и конфиденциальность
## 🔒 Privacy & Security
- **100% локальное хранение**: Вся история, закладки, локальные авторы и конфигурации хранятся только в вашей локальной базе на устройстве.
- **Никакой телеметрии**: Prisma не собирает аналитику, не отправляет логи и уважает вашу приватность.
- **Прямое соединение**: Запросы отправляются напрямую к выбранным API без прокси-серверов посредников.
- **100% On-Device Storage**: Your browsing history, bookmarks, favorite creators, and credentials stay strictly on your device.
- **Zero Telemetry**: No background tracking, no third-party analytic SDKs, and no user profiling.
- **Direct Network Requests**: Communication occurs directly between your client and selected provider endpoints without intermediary proxies.
---
## 👥 Разработчики
## 👥 Authors & Credits
<p align="center">
<strong>Разработчики:</strong> <a href="https://github.com/RarDog">RarDog</a> & <a href="https://deepmind.google/technologies/gemini/">Antigravity (Gemini)</a>
<strong>Authors:</strong> <a href="https://github.com/RarDog">RarDog</a> & <a href="https://deepmind.google/technologies/gemini/">Antigravity (Gemini)</a>
</p>
<p align="center">
<a href="https://github.com/RarDog"><img src="https://img.shields.io/badge/Developer-RarDog-blue?style=for-the-badge&logo=github&logoColor=white" alt="RarDog" /></a>
<a href="https://deepmind.google/technologies/gemini/"><img src="https://img.shields.io/badge/AI%20Pair-Antigravity%20(Gemini)-8A2BE2?style=for-the-badge&logo=google&logoColor=white" alt="Antigravity" /></a>
</p>
---
## ⚠️ Disclaimer & Vibe Coding
> [!NOTE]
> This project is 100% **vibe coding** built in tandem with an AI coding partner. I have zero background in Dart and Flutter — this project is built for personal use out of pure enthusiasm.
>
> Although every build is personally and thoroughly tested in daily use before being published, the codebase may still contain quirks, bugs, or sub-optimal patterns. This is a passion project built for personal fun and convenience, which means development could slow down or be abandoned at any moment without prior notice. Community contributions, advice, and pull requests are always welcome!
+192
View File
@@ -0,0 +1,192 @@
<p align="center">
<a href="README.md">English</a> • <strong>Русский</strong>
</p>
# 💎 Prisma
<p align="center">
<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">
<strong>Современный кроссплатформенный клиент для Booru и Creator-архивов нового поколения</strong><br>
Плавный Pinterest-стиль, интеграция Pawchive 2.0, двусторонняя синхронизация, интеллектуальное автообновление и воспроизведение медиа без задержек.
</p>
<p align="center">
<a href="https://github.com/RarDog/Prisma/releases"><img src="https://img.shields.io/badge/Version-3.5.4-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.5.4" /></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%20%7C%20macOS-00C853?style=for-the-badge" alt="Platforms" />
<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>
---
## 🚀 Что нового в Prisma 3.5
### 🎨 Дизайн 2.0 и оптимизация интерфейса
- **Современный визуальный стиль**: Полный переход на squircle-карточки, адаптивную цветовую палитру и улучшенную Material You динамическую тему.
- **Интеллектуальный Action Dock 2.0**: Компактный док ключевых действий под контентом с быстрым доступом к скачиванию, шерингу, тегам и инфо.
- **Плавные анимации и микро-взаимодействия**: Открытие медиа с эффектом Hero, интерактивное листание свайпами и анимированный лайк по двойному тапу.
- **Умная работа с экранной клавиатурой**: Автоматическое скрытие плавающих элементов при вводе и закрытие клавиатуры при прокрутке списков.
### 🐾 Глубокая интеграция Pawchive 2.0
- **Мульти-аккаунты**: Поддержка авторизации по логину и паролю, а также прямому вводу `Session Cookie`.
- **Двусторонняя синхронизация (Two-Way Sync)**: Автоматическая синхронизация избранного между локальной базой Prisma и профилем Pawchive. Новые авторы с сервера добавляются в приложение, а локальные авторы выгружаются на сервер в один тап.
- **Моментальная синхронизация**: При нажатии кнопки «В избранное» у любого автора (Fanbox, Patreon, Fantia, Boosty, Gumroad, DLsite, Discord) статус синхронизируется с сервером на лету.
### 🎬 Умный медиаплеер и определение разрешения
- **Ленивое вычисление габаритов (1920×1080 и др.)**: Определение точного разрешения картинок и видео без полной предварительной загрузки тяжелых файлов.
- **Автоповорот в полноэкранном режиме**: Видеоплеер автоматически ориентирует экран (альбомный или портретный режим) в зависимости от аспектного соотношения медиа.
### 📦 Разделение APK и умное автообновление по ABI
- Сборка облегчённых APK с разделением по архитектурам процессора (`arm64-v8a`, `armeabi-v7a`, `x86_64`).
- Встроенная система обновлений автоматически определяет архитектуру вашего Android-устройства и скачивает только нужный APK, экономя трафик и ускоряя загрузку.
---
## 📚 Документация и Вики (Руководство пользователя)
Подробные иллюстрированные руководства доступны в **[Prisma Wiki](https://github.com/RarDog/Prisma/wiki)** и прямо в репозитории:
- 🚀 **[Быстрый старт и установка](docs/wiki/Getting-Started.md)** — выбор правильного APK под процессор, запуск на Linux, Windows и macOS.
- 🌐 **[Провайдеры контента](docs/wiki/Providers.md)** — особенности Booru (Gelbooru, Rule34, Realbooru, e621) и архивов Pawchive.
- 🔍 **[Умный поиск и теги](docs/wiki/Search-and-Tags.md)** — оператор `and`, общие модификаторы и подсказки за 0 мс.
- 🐾 **[Интеграция с Pawchive](docs/wiki/Pawchive-Sync.md)** — вход по паролю/куки, двусторонняя синхронизация и выгрузка авторов.
- 🎬 **[Медиаплеер и просмотр](docs/wiki/Media-Viewer.md)** — жесты, автоповорот видео, разрешение 1920×1080 и Action Dock 2.0.
- 💾 **[Офлайн-режим и загрузки](docs/wiki/Offline-and-Downloads.md)** — Sync Hub, шаблоны папок и сохранение оригиналов.
- 🛡️ **[Черный список и фильтрация](docs/wiki/Safety-and-Blacklist.md)** — настройка Blacklist, Safe Mode и очистка кэша.
- ❓ **[Часто задаваемые вопросы (FAQ)](docs/wiki/FAQ.md)** — автообновление, звук в видео и решение проблем.
---
## ✨ Ключевые возможности
- **📱 Плавная лента в стиле Pinterest**: Masonry-сетка с переключением режима отображения (Masonry, 1:1 Сетка, Компактный список).
- **💾 Офлайн 2.0 и Sync Hub**: Просмотр фото и видео прямо из локального хранилища без запросов в сеть, аналитика занятого места и пакетная выгрузка.
- **🔍 Поиск 2.0 с операторами**: Быстрые подсказки тегов со скоростью отклика 0 мс, оператор `and` для мульти-поиска, фильтры `type:video`, `rating:safe`.
- **🛡️ Продвинутый Blacklist / Whitelist**: Фильтрация нежелательного контента по тегам, категориям, рейтингу и минимальному score.
- **📁 Умный менеджер загрузок**: Шаблоны поддиректорий (`{Artist}`, `{Provider}`, `{Service}`, `{ID}`, `{Date}`) для аккуратного архива.
- **🌑 AMOLED Pure Black**: Истинно черный фон для максимальной энергоэффективности на OLED/AMOLED дисплеях.
---
## 🌐 Поддерживаемые провайдеры
| Провайдер | Тип API | Поддерживаемый контент |
|---|---|---|
| **Pawchive** | REST API v1 | Patreon, Pixiv Fanbox, Fantia, Boosty, Gumroad, DLsite, Discord |
| **Gelbooru** | JSON / XML API | Аниме-арты, теги, авторы, рейтинги |
| **Rule34** | Booru API | Огромная база booru-контента, видео и GIF |
| **Realbooru** | Scraper / API | Фотосессии и реалистичный контент |
| **e621 / e926** | REST JSON API | Фурри- и антро-арт с богатейшей таксономией тегов |
---
## 📥 Загрузка (Скачать сборки)
Релизы доступны на странице **[GitHub Releases](https://github.com/RarDog/Prisma/releases)**.
| Платформа | Архитектура | Формат | Описание |
|---|---|---|---|
| **Android** | `arm64-v8a` | `.apk` | Оптимизировано для современных смартфонов (64-bit) |
| **Android** | `armeabi-v7a` | `.apk` | Для более старых Android-устройств (32-bit) |
| **Android** | `x86_64` | `.apk` | Для эмуляторов Android и x86-планшетов |
| **Linux** | `x86_64` | `.tar.gz` | Портативный архив для Linux-дистрибутивов |
| **Windows** | `x64` | `.zip` | Портативная сборка для Windows 10/11 |
| **macOS** | `Universal` | `.zip` | Сборка для macOS |
---
## 🛠️ Сборка из исходников
### Требования
- **Flutter SDK**: `>= 3.22.0` (Dart `>= 3.4.0`)
- **Android SDK**: Build-Tools & Platform-Tools (для Android)
- **Linux**: `clang`, `cmake`, `ninja-build`, `pkg-config`, `libgtk-3-dev`
- **Windows**: Visual Studio 2022 с компонентом «Desktop development with C++»
- **macOS**: Xcode 15+
### Быстрый старт
```bash
# Клонирование репозитория
git clone https://github.com/RarDog/Prisma.git
cd Prisma
# Установка зависимостей
flutter pub get
# Проверка анализатора и запуск тестов
flutter analyze
flutter test
# Запуск в режиме разработки
flutter run
```
### Сборка релизных пакетов
```bash
# Android (раздельные APK по архитектурам)
flutter build apk --release --split-per-abi
# Linux (Desktop bundle)
flutter build linux --release
# Windows (Desktop bundle)
flutter build windows --release
# macOS (Desktop bundle)
flutter build macos --release
```
---
## 🏛️ Архитектура проекта
```text
lib/
├── app/ # Роутер (GoRouter), темы, локализация (RU/EN), анимации
├── backend/
│ ├── di/ # Внедрение зависимостей (Riverpod providers)
│ ├── models/ # Post, ArtistProfile, PawchiveAccount, AppSettings
│ ├── providers/ # Клиенты API (Gelbooru, Rule34, Pawchive, e621)
│ ├── repositories/ # Локальная база данных Isar и кэширование
│ └── services/ # Сервисы: PawchiveSync, Feed, Search, Downloads, Updates
├── core/ # Сетевой клиент (Dio), база данных, утилиты
├── features/ # Модули экранов: feed, post, artists, favorites, settings
└── shared/widgets/ # Переиспользуемые компоненты (PostCard, MasonryGrid, Dock)
```
---
## 🔒 Безопасность и конфиденциальность
- **100% локальное хранение**: Вся история, закладки, локальные авторы и конфигурации хранятся только в вашей локальной базе на устройстве.
- **Никакой телеметрии**: Prisma не собирает аналитику, не отправляет логи и уважает вашу приватность.
- **Прямое соединение**: Запросы отправляются напрямую к выбранным API без прокси-серверов посредников.
---
## 👥 Разработчики
<p align="center">
<strong>Разработчики:</strong> <a href="https://github.com/RarDog">RarDog</a> & <a href="https://deepmind.google/technologies/gemini/">Antigravity (Gemini)</a>
</p>
<p align="center">
<a href="https://github.com/RarDog"><img src="https://img.shields.io/badge/Developer-RarDog-blue?style=for-the-badge&logo=github&logoColor=white" alt="RarDog" /></a>
<a href="https://deepmind.google/technologies/gemini/"><img src="https://img.shields.io/badge/AI%20Pair-Antigravity%20(Gemini)-8A2BE2?style=for-the-badge&logo=google&logoColor=white" alt="Antigravity" /></a>
</p>
---
## ⚠️ Важное примечание / Vibe Coding
> [!NOTE]
> Этот проект — чистый **vibe coding** в связке с AI-ассистентом. Я полный ноль в Dart и Flutter, проект пишется для себя на чистом энтузиазме.
>
> Несмотря на то, что каждое обновление лично и тщательно тестируется перед релизом, приложение может содержать баги, шероховатости или неоптимальные решения. Проект создаётся для души и личного удобства, поэтому разработка может быть замедлена или заброшена в любой момент. Пулл-реквесты, советы и помощь сообщества всегда приветствуются!
+1
View File
@@ -7,6 +7,7 @@ analyzer:
- android/**
- windows/**
- linux/**
- macos/**
linter:
rules:
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.4.1';
const appBuildNumber = 75;
const appDisplayVersion = '3.5.4';
const appBuildNumber = 80;
const appReleaseChannel = 'Stable';
+47
View File
@@ -1,4 +1,51 @@
const prismaChangelog = [
PrismaChange(
version: '3.5.4',
title: 'Исправление распознавания MP4-видео и отключение устаревших сервисов Kemono/Coomer',
bullets: [
'Устранена ошибка классификации видео: файлы MP4, содержащие слово «audio» в имени файла или параметрах запроса, больше не распознаются как аудио и корректно воспроизводятся как полноценное видео.',
'Точное определение расширений: внедрён строгий парсинг расширений файлов, исключающий ложные срабатывания при определении типа медиа.',
'Удаление устаревших провайдеров Kemono и Coomer: окончательно удалены неработающие сервисы Kemono и Coomer из списка провайдеров и настроек (основным источником артистов является Pawchive).',
],
),
PrismaChange(
version: '3.5.3',
title: 'Поддержка аудиозаписей (MP3/WAV/FLAC) и исправление загрузки медиа у авторов',
bullets: [
'Полноценная поддержка аудио: добавлено воспроизведение MP3, M4A, WAV, FLAC, OGG, AAC и OPUS файлов авторов (Patreon, Fanbox и др.).',
'Встроенный аудиопроигрыватель: карточка с анимированной пластинкой/обложкой, таймлайном, перемоткой на 10 сек, повтором и регулятором громкости.',
'Устранение ошибки 404 у миниатюр: исправлена попытка загрузки несуществующих эскизов для аудио и видео с Pawchive, из-за чего файлы отображались как битые.',
'Фильтр аудиозаписей: во вкладке артиста добавлен фильтр «Аудио», позволяющий мгновенно находить все треки и голосовые посты автора.',
],
),
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: 'Полный ребрендинг интерфейса: заголовок ленты, окно «О приложении» и системные службы',
-625
View File
@@ -1,625 +0,0 @@
import 'package:dio/dio.dart';
import '../../core/http/dio_client.dart';
import '../models/artist_announcement.dart';
import '../models/artist_link.dart';
import '../models/artist_profile.dart';
import '../models/artist_tag.dart';
import '../models/artist_work_query.dart';
import '../models/post.dart';
import '../models/post_comment.dart';
import '../models/provider_health.dart';
import '../models/top_period_filter.dart';
import '../services/cloud_link_extractor.dart';
import 'content_provider.dart';
class KemonoProvider
implements ContentProvider, ArtistProvider, CommentProvider {
KemonoProvider({
required this.id,
required this.name,
required this.baseUrl,
required DioClient dioClient,
Map<String, String> queryParameters = const {},
}) : _dio = dioClient.dio,
_creatorDio = Dio(
BaseOptions(
baseUrl: queryParameters['creator_api_base'] ??
'https://kemono-api.mbaharip.com',
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 20),
headers: const {
'Accept': 'application/json',
'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
},
),
);
@override
final String id;
@override
final String name;
@override
final String baseUrl;
final Dio _dio;
final Dio _creatorDio;
bool get _isCoomer =>
id.toLowerCase().contains('coomer') ||
baseUrl.toLowerCase().contains('coomer');
@override
Future<List<Post>> searchPosts({
required List<String> tags,
required int page,
int limit = 50,
String? rating,
TopPeriodFilter topPeriod = TopPeriodFilter.none,
}) async {
return const [];
}
@override
Future<Post?> getPost(String id) async => null;
@override
Future<ProviderHealth> checkHealth() async {
final startedAt = DateTime.now();
try {
await _creatorDio.get<dynamic>(
_creatorPath(),
queryParameters: {'page': 1, 'itemsPerPage': 1},
);
return ProviderHealth(
providerId: id,
status: ProviderStatus.online,
pingMs: DateTime.now().difference(startedAt).inMilliseconds,
lastCheckedAt: DateTime.now(),
apiVersion: _isCoomer ? 'coomer-artists' : 'kemono-artists',
);
} catch (error) {
return ProviderHealth(
providerId: id,
status: ProviderStatus.offline,
pingMs: DateTime.now().difference(startedAt).inMilliseconds,
lastCheckedAt: DateTime.now(),
errorMessage: error.toString(),
apiVersion: _isCoomer ? 'coomer-artists' : 'kemono-artists',
);
}
}
@override
Future<List<ArtistProfile>> listArtists({
String? service,
List<String>? services,
String? query,
int page = 1,
int limit = 30,
}) async {
final effectiveService =
services != null && services.length == 1 ? services.first : service;
List<ArtistProfile> list;
try {
final response = await _creatorDio.get<dynamic>(
effectiveService == null || effectiveService.isEmpty
? _creatorPath()
: '${_creatorPath()}/$effectiveService',
queryParameters: {
'page': page,
'itemsPerPage': limit.clamp(1, 100),
if (query != null && query.trim().isNotEmpty) 'keyword': query.trim(),
},
);
list = _rankArtists(_artistsFromResponse(response.data), query ?? '');
} catch (_) {
list = await _listArtistsFromPublicApi(
service: effectiveService,
query: query,
page: page,
limit: limit,
);
}
if (services != null && services.isNotEmpty) {
final sSet = services.map((s) => s.toLowerCase()).toSet();
list = list.where((a) => sSet.contains(a.service.toLowerCase())).toList();
}
return list;
}
@override
Future<List<ArtistProfile>> searchArtists(
String query, {
String? service,
List<String>? services,
int page = 1,
int limit = 30,
}) {
return listArtists(
service: service,
services: services,
query: query,
page: page,
limit: limit,
);
}
@override
Future<List<Post>> getArtistPosts({
required ArtistWorkQuery query,
int page = 0,
int limit = 50,
}) async {
Object? lastError;
final paths = [
'/api/v1/${query.service}/user/${query.artistId}/posts',
'/api/v1/${query.service}/user/${query.artistId}',
];
for (final apiBase in _apiBaseCandidates()) {
for (final path in paths) {
try {
final response = await _getFromApiBase<dynamic>(
apiBase,
path,
queryParameters: {'o': page * limit},
);
final posts = _postItems(response.data);
final mapped = <Post>[];
for (final post in posts.whereType<Map>()) {
mapped.addAll(_postsFromArtistPost(
Map<String, dynamic>.from(post),
query: query,
mediaBaseUrl: apiBase,
));
}
if (mapped.isNotEmpty || posts.isEmpty) {
mapped.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return mapped.take(limit).toList(growable: false);
}
} catch (error) {
lastError = error;
}
}
}
throw StateError(
'$name artist works are unavailable right now. Try again later. '
'${_shortError(lastError)}',
);
}
@override
Future<List<PostComment>> getArtistPostComments(
String service,
String artistId,
String postId,
) async {
Object? lastError;
final path = '/api/v1/$service/user/$artistId/post/$postId/comments';
for (final apiBase in _apiBaseCandidates()) {
try {
final response = await _getFromApiBase<dynamic>(apiBase, path);
return _commentsFromResponse(response.data, postId);
} catch (error) {
lastError = error;
}
}
throw StateError(
'$name comments are unavailable right now. ${_shortError(lastError)}',
);
}
@override
Future<List<PostComment>> getComments(String postId) async {
final parts = postId.split(':');
if (parts.length < 4) return const [];
return getArtistPostComments(parts[0], parts[1], parts[2]);
}
@override
Future<List<ArtistTag>> getArtistTags(
String service, String artistId) async =>
const [];
@override
Future<List<ArtistLink>> getArtistLinks(
String service, String artistId) async =>
const [];
@override
Future<List<ArtistAnnouncement>> getArtistAnnouncements(
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';
List<ArtistProfile> _artistsFromResponse(dynamic data) {
final items = data is Map ? data['data'] : data;
if (items is! List) return const [];
return items
.whereType<Map>()
.map((item) {
final json = Map<String, dynamic>.from(item);
final service = (json['service'] ?? '').toString();
final artistId =
(json['id'] ?? json['creator_id'] ?? json['user_id'] ?? '')
.toString();
final name = (json['name'] ??
json['display_name'] ??
json['username'] ??
artistId)
.toString();
final rawAvatar = (json['avatar'] ??
json['icon'] ??
json['image'] ??
json['avatar_url'] ??
'')
.toString();
return ArtistProfile(
id: artistId,
providerId: id,
service: service,
name: name,
displayName: name,
avatarUrl: _absoluteUrl(rawAvatar) ?? _iconUrl(service, artistId),
updatedAt: _dateFromAny(json['updated']),
postCount: (json['post_count'] as num?)?.toInt(),
url: '$baseUrl/$service/user/$artistId',
);
})
.where((artist) => artist.id.isNotEmpty)
.toList(growable: false);
}
Future<List<ArtistProfile>> _listArtistsFromPublicApi({
String? service,
String? query,
required int page,
required int limit,
}) async {
Object? lastError;
dynamic data;
for (final apiBase in _apiBaseCandidates()) {
try {
final response =
await _getFromApiBase<dynamic>(apiBase, '/api/v1/creators.txt');
data = response.data;
break;
} catch (error) {
lastError = error;
}
}
if (data == null) {
throw StateError(
'$name artist index is unavailable right now. ${_shortError(lastError)}',
);
}
final queryText = (query ?? '').trim().toLowerCase();
final offset = (page - 1).clamp(0, 999999) * limit;
final items = _artistsFromResponse({'data': data})
.where((artist) =>
(service == null || service.isEmpty || artist.service == service) &&
(queryText.isEmpty ||
artist.displayName.toLowerCase().contains(queryText) ||
artist.id.toLowerCase().contains(queryText)))
.toList(growable: false);
final ranked = _rankArtists(items, queryText);
return ranked.skip(offset).take(limit).toList(growable: false);
}
List<ArtistProfile> _rankArtists(List<ArtistProfile> artists, String query) {
final queryText = query.trim().toLowerCase();
final ranked = [...artists];
ranked.sort((a, b) {
final scoreA = _artistSearchScore(a, queryText);
final scoreB = _artistSearchScore(b, queryText);
if (scoreA != scoreB) return scoreB.compareTo(scoreA);
final updatedA = a.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0);
final updatedB = b.updatedAt ?? DateTime.fromMillisecondsSinceEpoch(0);
final updated = updatedB.compareTo(updatedA);
if (updated != 0) return updated;
final count = (b.postCount ?? 0).compareTo(a.postCount ?? 0);
if (count != 0) return count;
return a.displayName.compareTo(b.displayName);
});
return ranked;
}
int _artistSearchScore(ArtistProfile artist, String query) {
if (query.isEmpty) return 0;
final name = artist.displayName.toLowerCase();
final id = artist.id.toLowerCase();
if (name == query || id == query) return 100;
if (name.startsWith(query) || id.startsWith(query)) return 70;
if (name.contains(query) || id.contains(query)) return 40;
return 0;
}
List<dynamic> _postItems(dynamic data) {
if (data is List) return data;
if (data is Map) {
final results = data['results'];
if (results is List) return results;
final posts = data['posts'];
if (posts is List) return posts;
if (posts is Map) return _postItems(posts);
final dataItems = data['data'];
if (dataItems is List) return dataItems;
}
return const [];
}
List<Post> _postsFromArtistPost(
Map<String, dynamic> json, {
required ArtistWorkQuery query,
required String mediaBaseUrl,
}) {
final postId = (json['id'] ?? json['post_id'] ?? '').toString();
final title = (json['title'] ?? '').toString();
final published = _dateFromAny(json['published']) ??
_dateFromAny(json['added']) ??
_dateFromAny(json['edited']) ??
DateTime.fromMillisecondsSinceEpoch(0);
final postSource =
'$baseUrl/${query.service}/user/${query.artistId}/post/$postId';
final rawContent = (json['content'] ?? '').toString();
final embed = json['embed'] is Map ? Map<String, dynamic>.from(json['embed']) : null;
final cloudLinks = CloudLinkExtractor.extractLinks(
content: rawContent,
source: postSource,
embed: embed,
);
final cloudLinksJson = cloudLinks.map((e) => e.encode()).toList(growable: false);
final cleanDesc = CloudLinkExtractor.cleanCommentary(rawContent);
final files = <Map<String, dynamic>>[];
final file = json['file'];
if (file is Map && file.isNotEmpty) {
files.add(Map<String, dynamic>.from(file));
}
final attachments = json['attachments'];
if (attachments is List) {
files.addAll(attachments.whereType<Map>().map(Map<String, dynamic>.from));
}
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
// 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(),
if (!isTextOnly) 'cloud_mirror' else 'text_post',
];
return [
Post(
id: stableId,
providerId: id,
providerName: name,
previewUrl: '',
sampleUrl: '',
fileUrl: fileUrl,
tags: tags,
rating: 'unknown',
width: 0,
height: 0,
source: postSource,
createdAt: published,
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]
else if (rawContent.trim().isNotEmpty)
'description': [rawContent.trim()],
},
),
];
}
var index = 0;
return files
.map((file) {
final url = _mediaUrl(file, mediaBaseUrl: mediaBaseUrl);
final fileName =
(file['name'] ?? Uri.tryParse(url)?.pathSegments.last ?? '')
.toString();
final type = _fileType('$fileName $url');
final stableId =
'${query.service}:${query.artistId}:$postId:${index++}';
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
];
return Post(
id: stableId,
providerId: id,
providerName: name,
previewUrl: url,
sampleUrl: url,
fileUrl: url,
tags: tags,
rating: 'unknown',
width: 0,
height: 0,
source: postSource,
createdAt: published,
fileType: type,
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],
},
);
})
.where((post) => post.fileUrl.isNotEmpty)
.toList(growable: false);
}
List<PostComment> _commentsFromResponse(dynamic data, String postId) {
final items = data is Map ? (data['comments'] ?? data['data']) : data;
if (items is! List) return const [];
return items
.whereType<Map>()
.map((item) {
final json = Map<String, dynamic>.from(item);
return PostComment(
id: (json['id'] ?? json['comment_id'] ?? '').toString(),
postId: postId,
providerId: id,
authorName: (json['author'] ??
json['username'] ??
json['user'] ??
json['creator_name'] ??
'user')
.toString(),
body: (json['content'] ?? json['body'] ?? json['comment'] ?? '')
.toString(),
createdAt: _dateFromAny(json['added']) ??
_dateFromAny(json['created_at']) ??
DateTime.fromMillisecondsSinceEpoch(0),
);
})
.where((comment) => comment.body.trim().isNotEmpty)
.toList(growable: false);
}
String _mediaUrl(
Map<String, dynamic> file, {
required String mediaBaseUrl,
}) {
final path = (file['path'] ?? file['url'] ?? '').toString();
if (path.isEmpty) return '';
final uri = Uri.tryParse(path);
if (uri != null && uri.hasScheme) return path;
if (path.startsWith('/data/')) {
return '$mediaBaseUrl$path';
}
if (path.startsWith('/')) {
return '$mediaBaseUrl/data$path';
}
return '$mediaBaseUrl/data/$path';
}
String? _iconUrl(String service, String artistId) {
if (service.isEmpty || artistId.isEmpty) return null;
final host = _isCoomer ? 'https://img.coomer.su' : 'https://img.kemono.su';
return '$host/icons/$service/$artistId';
}
String? _absoluteUrl(String value) {
final trimmed = value.trim();
if (trimmed.isEmpty) return null;
final uri = Uri.tryParse(trimmed);
if (uri != null && uri.hasScheme) return trimmed;
if (trimmed.startsWith('/')) return '$baseUrl$trimmed';
return '$baseUrl/$trimmed';
}
List<String> _apiBaseCandidates() {
final candidates = <String>[
baseUrl,
if (_isCoomer) ...[
'https://coomer.st',
'https://coomer.su',
] else ...[
'https://kemono.cr',
'https://kemono.su',
],
];
return candidates.map(_trimTrailingSlash).toSet().toList(growable: false);
}
Future<Response<T>> _getFromApiBase<T>(
String apiBase,
String path, {
Map<String, Object?> queryParameters = const {},
}) {
if (_trimTrailingSlash(apiBase) == _trimTrailingSlash(baseUrl)) {
return _dio.get<T>(path, queryParameters: queryParameters);
}
final dio = Dio(
BaseOptions(
baseUrl: apiBase,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 25),
headers: const {
'Accept': 'application/json',
'User-Agent': 'Prisma/2.0.1 Flutter local booru browser',
},
),
);
return dio.get<T>(path, queryParameters: queryParameters);
}
String _trimTrailingSlash(String value) =>
value.endsWith('/') ? value.substring(0, value.length - 1) : value;
String _shortError(Object? error) {
if (error == null) return '';
final message = error.toString();
if (message.length <= 160) return message;
return '${message.substring(0, 160)}...';
}
String _fileType(String value) {
final lower = value.toLowerCase().split('?').first;
if (lower.endsWith('.webm') || lower.endsWith('.mp4')) return 'video';
if (lower.endsWith('.gif')) return 'gif';
if (lower.endsWith('.swf')) return 'swf';
if (lower.endsWith('.png')) return 'png';
if (lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.webp')) {
return 'photo';
}
return 'unknown';
}
DateTime? _dateFromAny(dynamic value) {
if (value == null) return null;
if (value is num) {
final raw = value.toInt();
if (raw > 9999999999) return DateTime.fromMillisecondsSinceEpoch(raw);
return DateTime.fromMillisecondsSinceEpoch(raw * 1000);
}
return DateTime.tryParse(value.toString());
}
}
+99 -13
View File
@@ -414,7 +414,15 @@ class PawchiveProvider
}
final attachments = json['attachments'];
if (attachments is List) {
files.addAll(attachments.whereType<Map>().map(Map<String, dynamic>.from));
for (final att in attachments.whereType<Map>()) {
final m = Map<String, dynamic>.from(att);
final path = (m['path'] ?? '').toString();
if (path.isNotEmpty &&
files.any((existing) => (existing['path'] ?? '').toString() == path)) {
continue;
}
files.add(m);
}
}
// Support link-only posts where author uploaded files to cloud drives (MEGA, GDrive, etc.)
@@ -468,6 +476,17 @@ class PawchiveProvider
];
}
// Determine if there is a cover illustration for the post
String? postCoverUrl;
for (final f in files) {
final p = (f['path'] ?? '').toString();
final n = (f['name'] ?? '').toString();
if (_isPhotoType(p, n) && p.isNotEmpty) {
postCoverUrl = _thumbnailUrl(p);
break;
}
}
var index = 0;
return files
.map((fileMap) {
@@ -475,18 +494,26 @@ class PawchiveProvider
final fileName = (fileMap['name'] ?? '').toString();
final previewOnly = fileMap['preview_only'] == true;
final previewUrl = _thumbnailUrl(rawPath);
final type = _fileTypeFor(rawPath, fileName);
final isPhoto = type == 'photo' || type == 'gif';
// img.pawchive.pw/thumbnail only generates thumbnails for images/gifs.
// Non-image files (audio, video, archive) return 404 from thumbnail server.
final previewUrl = isPhoto
? _thumbnailUrl(rawPath)
: (postCoverUrl ?? '');
final fileUrl = previewOnly
? previewUrl
? (isPhoto ? previewUrl : _fileDownloadUrl(rawPath, fileName))
: _fileDownloadUrl(rawPath, fileName);
final type = _fileType('$fileName $rawPath');
final stableId =
'${query.service}:${query.artistId}:$postId:${index++}';
final tags = [
query.service,
query.artistName,
if (title.trim().isNotEmpty) title.trim(),
if (type == 'audio') 'audio',
];
return Post(
@@ -599,25 +626,84 @@ class PawchiveProvider
return '${message.substring(0, 160)}...';
}
String _extractExtension(String value) {
if (value.isEmpty) return '';
final clean = value.split('?').first.split('#').first.trim();
final dot = clean.lastIndexOf('.');
if (dot != -1 && dot < clean.length - 1) {
final ext = clean.substring(dot).toLowerCase();
if (ext.length <= 6) return ext;
}
return '';
}
String _fileType(String value) {
final lower = value.toLowerCase().split('?').first;
if (lower.endsWith('.webm') || lower.endsWith('.mp4') || lower.endsWith('.mov')) {
final ext = _extractExtension(value);
if (const {
'.webm',
'.mp4',
'.mov',
'.mkv',
'.avi',
'.flv',
'.wmv',
'.m4v',
'.ts',
}.contains(ext)) {
return 'video';
}
if (lower.endsWith('.gif')) return 'gif';
if (lower.endsWith('.swf')) return 'swf';
if (lower.endsWith('.zip') || lower.endsWith('.rar') || lower.endsWith('.7z')) {
if (const {
'.mp3',
'.m4a',
'.wav',
'.ogg',
'.flac',
'.aac',
'.opus',
'.wma',
}.contains(ext)) {
return 'audio';
}
if (ext == '.gif') return 'gif';
if (ext == '.swf') return 'swf';
if (const {
'.zip',
'.rar',
'.7z',
'.tar',
'.gz',
'.pdf',
'.txt',
}.contains(ext)) {
return 'archive';
}
if (lower.endsWith('.png') ||
lower.endsWith('.jpg') ||
lower.endsWith('.jpeg') ||
lower.endsWith('.webp')) {
if (const {
'.png',
'.jpg',
'.jpeg',
'.webp',
'.avif',
'.bmp',
'.heic',
'.tiff',
}.contains(ext)) {
return 'photo';
}
return 'unknown';
}
String _fileTypeFor(String path, String fileName) {
final extName = _extractExtension(fileName);
final extPath = _extractExtension(path);
final ext = extName.isNotEmpty ? extName : extPath;
return _fileType(ext);
}
bool _isPhotoType(String path, [String fileName = '']) {
final type = _fileTypeFor(path, fileName);
return type == 'photo' || type == 'gif';
}
DateTime? _dateFromAny(dynamic value) {
if (value == null) return null;
if (value is num) {
@@ -5,7 +5,6 @@ import 'custom_provider.dart';
import 'danbooru_provider.dart';
import 'e621_provider.dart';
import 'gelbooru_provider.dart';
import 'kemono_provider.dart';
import 'moebooru_provider.dart';
import 'pawchive_provider.dart';
import 'realbooru_html_provider.dart';
@@ -82,15 +81,6 @@ class ProviderFactory {
dioClient: client,
queryParameters: queryParameters,
);
case 'kemono':
case 'coomer':
return KemonoProvider(
id: config.id,
name: config.name,
baseUrl: config.baseUrl,
dioClient: client,
queryParameters: queryParameters,
);
case 'pawchive':
return PawchiveProvider(
id: config.id,
+8 -3
View File
@@ -730,18 +730,23 @@ class ProviderManager {
static bool _isArtistConfig(ContentProviderConfig config) {
final type = config.apiType.toLowerCase();
return type == 'kemono' || type == 'coomer' || type == 'pawchive';
return type == 'pawchive';
}
static bool _isFeedConfig(ContentProviderConfig config) {
final type = config.apiType.toLowerCase();
return type != 'kemono' && type != 'coomer';
return type != 'pawchive';
}
static bool _isLegacyRemovedConfig(ContentProviderConfig config) {
final id = config.id.toLowerCase();
final type = config.apiType.toLowerCase();
return id == 'cosbooru' || type == 'realbooru';
return id == 'cosbooru' ||
type == 'realbooru' ||
type == 'kemono' ||
type == 'coomer' ||
id == 'kemono' ||
id == 'coomer';
}
}
+112 -11
View File
@@ -80,7 +80,16 @@ class MediaUrlSelector {
]);
}
static List<String> audio(Post post) {
return _compact([
post.fileUrl,
if (_looksLikeAudioUrl(post.sampleUrl)) post.sampleUrl,
if (_looksLikeAudioUrl(post.previewUrl)) post.previewUrl,
]);
}
static bool isVideo(Post post) => _isVideo(post);
static bool isAudio(Post post) => _isAudio(post);
static bool isGif(Post post) => _isGif(post);
static List<String> _compact(Iterable<String> urls) {
@@ -91,23 +100,115 @@ class MediaUrlSelector {
.toList(growable: false);
}
static const _videoExtensions = {
'.mp4',
'.webm',
'.mov',
'.mkv',
'.avi',
'.flv',
'.wmv',
'.m4v',
'.ts',
};
static const _audioExtensions = {
'.mp3',
'.m4a',
'.wav',
'.ogg',
'.flac',
'.aac',
'.opus',
'.wma',
};
static String _extractExtension(String url) {
if (url.isEmpty) return '';
try {
final uri = Uri.parse(url);
final filenameParam =
uri.queryParameters['f'] ?? uri.queryParameters['filename'];
if (filenameParam != null && filenameParam.isNotEmpty) {
final cleanParam = filenameParam.split('?').first.split('#').first;
final dot = cleanParam.lastIndexOf('.');
if (dot != -1 && dot < cleanParam.length - 1) {
final ext = cleanParam.substring(dot).toLowerCase();
if (ext.length <= 6) return ext;
}
}
final path = uri.path;
final dot = path.lastIndexOf('.');
if (dot != -1 && dot < path.length - 1) {
final ext = path.substring(dot).toLowerCase();
if (ext.length <= 6) return ext;
}
} catch (_) {
// Uri parsing failed, fallback below
}
final clean = url.split('?').first.split('#').first;
final dot = clean.lastIndexOf('.');
if (dot != -1 && dot < clean.length - 1) {
final ext = clean.substring(dot).toLowerCase();
if (ext.length <= 6) return ext;
}
return '';
}
static bool _isAudio(Post post) {
final type = post.fileType.toLowerCase().trim();
if (type == 'audio') return true;
if (type == 'video' || type == 'photo' || type == 'archive' || type == 'gif') {
return false;
}
final fileExt = _extractExtension(post.fileUrl);
if (_audioExtensions.contains(fileExt)) return true;
if (_videoExtensions.contains(fileExt)) return false;
final sampleExt = _extractExtension(post.sampleUrl);
if (_audioExtensions.contains(sampleExt)) return true;
if (_videoExtensions.contains(sampleExt)) return false;
return false;
}
static bool _isVideo(Post post) {
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
return value.contains('video') ||
value.contains('.webm') ||
value.contains('.mp4') ||
value.contains('.mov');
final type = post.fileType.toLowerCase().trim();
if (type == 'video') return true;
if (type == 'audio' || type == 'photo' || type == 'archive' || type == 'gif') {
return false;
}
final fileExt = _extractExtension(post.fileUrl);
if (_videoExtensions.contains(fileExt)) return true;
if (_audioExtensions.contains(fileExt)) return false;
final sampleExt = _extractExtension(post.sampleUrl);
if (_videoExtensions.contains(sampleExt)) return true;
if (_audioExtensions.contains(sampleExt)) return false;
return false;
}
static bool _isGif(Post post) {
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
return value.contains('gif') || value.contains('.gif');
final type = post.fileType.toLowerCase().trim();
if (type == 'gif') return true;
final fileExt = _extractExtension(post.fileUrl);
if (fileExt == '.gif') return true;
final sampleExt = _extractExtension(post.sampleUrl);
return sampleExt == '.gif';
}
static bool _looksLikeVideoUrl(String url) {
final value = url.toLowerCase();
return value.contains('.webm') ||
value.contains('.mp4') ||
value.contains('.mov');
final ext = _extractExtension(url);
return _videoExtensions.contains(ext);
}
static bool _looksLikeAudioUrl(String url) {
final ext = _extractExtension(url);
return _audioExtensions.contains(ext);
}
}
@@ -191,11 +191,13 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
bool _matchesTypeFilter(Post post) {
if (_selectedTypes.isEmpty) return true;
final isVid = MediaUrlSelector.isVideo(post);
final isAud = MediaUrlSelector.isAudio(post);
final isGif = MediaUrlSelector.isGif(post);
final isText = post.fileType == 'text';
final isPhoto = !isVid && !isGif && !isText;
final isPhoto = !isVid && !isAud && !isGif && !isText;
if (_selectedTypes.contains('video') && isVid) return true;
if (_selectedTypes.contains('audio') && isAud) return true;
if (_selectedTypes.contains('gif') && isGif) return true;
if (_selectedTypes.contains('photo') && isPhoto) return true;
if (_selectedTypes.contains('text') && isText) return true;
@@ -462,10 +464,12 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
Widget _buildMediaTypeFilters() {
final scheme = Theme.of(context).colorScheme;
final audioCount = _posts.where(MediaUrlSelector.isAudio).length;
final photoCount = _posts
.where((p) =>
p.fileType != 'text' &&
!MediaUrlSelector.isVideo(p) &&
!MediaUrlSelector.isAudio(p) &&
!MediaUrlSelector.isGif(p))
.length;
final videoCount = _posts.where(MediaUrlSelector.isVideo).length;
@@ -489,8 +493,8 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
avatar: Icon(Icons.photo_outlined,
size: 16,
color: _selectedTypes.contains('photo')
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
label: Text('Фото ($photoCount)'),
onSelected: (selected) {
setState(() {
@@ -508,8 +512,8 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
avatar: Icon(Icons.videocam_outlined,
size: 16,
color: _selectedTypes.contains('video')
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
label: Text('Видео ($videoCount)'),
onSelected: (selected) {
setState(() {
@@ -521,14 +525,35 @@ class _ArtistPostsScreenState extends ConsumerState<ArtistPostsScreen> {
});
},
),
if (audioCount > 0) ...[
const SizedBox(width: 8),
FilterChip(
selected: _selectedTypes.contains('audio'),
avatar: Icon(Icons.audiotrack_rounded,
size: 16,
color: _selectedTypes.contains('audio')
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
label: Text('Аудио ($audioCount)'),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedTypes.add('audio');
} else {
_selectedTypes.remove('audio');
}
});
},
),
],
const SizedBox(width: 8),
FilterChip(
selected: _selectedTypes.contains('gif'),
avatar: Icon(Icons.gif_rounded,
size: 20,
color: _selectedTypes.contains('gif')
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
? scheme.onPrimaryContainer
: scheme.onSurfaceVariant),
label: Text('GIF ($gifCount)'),
onSelected: (selected) {
setState(() {
File diff suppressed because it is too large Load Diff
@@ -114,7 +114,8 @@ class PostDetailsScreen extends ConsumerWidget {
(post.previewUrl.isEmpty &&
post.sampleUrl.isEmpty &&
post.fileUrl.isEmpty &&
!MediaUrlSelector.isVideo(post));
!MediaUrlSelector.isVideo(post) &&
!MediaUrlSelector.isAudio(post));
final shouldShowCloudCard = isTextOnly
? post.cloudLinks.isNotEmpty
: (post.cloudLinks.isNotEmpty ||
@@ -230,17 +231,24 @@ class PostDetailsScreen extends ConsumerWidget {
),
Expanded(
child: Center(
child: MediaUrlSelector.isVideo(post)
child: (MediaUrlSelector.isVideo(post) ||
MediaUrlSelector.isAudio(post))
? ConstrainedBox(
constraints: const BoxConstraints(
maxHeight: 760,
constraints: BoxConstraints(
maxHeight: MediaUrlSelector.isAudio(post)
? 360
: 760,
maxWidth: MediaUrlSelector.isAudio(post)
? 620
: double.infinity,
),
child: AspectRatio(
aspectRatio:
(post.width > 0 && post.height > 0)
aspectRatio: MediaUrlSelector.isAudio(post)
? 1.4
: ((post.width > 0 && post.height > 0)
? (post.width / post.height)
.clamp(0.45, 2.4)
: (16 / 9),
: (16 / 9)),
child: PostMediaViewer(
key: ValueKey(post.cacheKey),
post: post,
@@ -433,6 +441,7 @@ class PostDetailsScreen extends ConsumerWidget {
List<Post>? feedPosts,
]) {
final isVideo = MediaUrlSelector.isVideo(post);
final isAudio = MediaUrlSelector.isAudio(post);
final localMedia =
ref.watch(downloadedMediaByKeyProvider(post.cacheKey)).value;
final fileSizeBytes = localMedia != null
@@ -450,7 +459,8 @@ class PostDetailsScreen extends ConsumerWidget {
(post.previewUrl.isEmpty &&
post.sampleUrl.isEmpty &&
post.fileUrl.isEmpty &&
!MediaUrlSelector.isVideo(post));
!MediaUrlSelector.isVideo(post) &&
!MediaUrlSelector.isAudio(post));
final shouldShowCloudCard = isTextOnly
? post.cloudLinks.isNotEmpty
: (post.cloudLinks.isNotEmpty ||
@@ -479,16 +489,20 @@ class PostDetailsScreen extends ConsumerWidget {
? null
: () =>
_showMobileQuickActions(context, ref, post, favoriteKeys),
child: isVideo
child: (isVideo || isAudio)
? Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.70,
maxHeight: isAudio
? 340
: MediaQuery.sizeOf(context).height * 0.70,
),
child: AspectRatio(
aspectRatio: (post.width > 0 && post.height > 0)
? (post.width / post.height).clamp(0.45, 2.4)
: (16 / 9),
aspectRatio: isAudio
? 1.3
: ((post.width > 0 && post.height > 0)
? (post.width / post.height).clamp(0.45, 2.4)
: (16 / 9)),
child: PostMediaViewer(
key: ValueKey(post.cacheKey),
post: post,
@@ -94,7 +94,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
_currentVolume = widget.initialVolume;
_imageUrls = _buildImageUrls(widget.post);
_videoUrls = _buildVideoUrls(widget.post);
if (_isVideo(widget.post) && _videoUrls.isNotEmpty) {
if (_isPlayableMedia(widget.post) && _videoUrls.isNotEmpty) {
_initializeVideo();
}
}
@@ -111,7 +111,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
_softwareDecodingFallback = false;
_imageUrls = _buildImageUrls(widget.post);
_videoUrls = _buildVideoUrls(widget.post);
if (_isVideo(widget.post) && _videoUrls.isNotEmpty) {
if (_isPlayableMedia(widget.post) && _videoUrls.isNotEmpty) {
_initializeVideo();
}
} else if (oldWidget.mediaHeaders != widget.mediaHeaders) {
@@ -160,7 +160,38 @@ class _PostMediaViewerState extends State<PostMediaViewer>
return const _UnsupportedSwfPanel();
}
if (_controller != null) {
if (_controller != null || _player != null) {
if (_isAudio(widget.post) && _player != null) {
return _AudioSurface(
player: _player!,
post: widget.post,
muted: _muted,
loopAudio: _loopVideo,
initialVolume: _currentVolume,
onVolumeChanged: (vol) {
_currentVolume = vol;
widget.onVolumeChanged?.call(vol);
_emitPlaybackPreferences();
},
fullscreen: widget.fullscreen,
errorMessage: _videoError,
onRetry: _retryVideo,
onToggleMute: () async {
final nextMuted = !_muted;
setState(() => _muted = nextMuted);
await _applyVolume();
_emitPlaybackPreferences();
},
onToggleLoop: () async {
final nextLoop = !_loopVideo;
setState(() => _loopVideo = nextLoop);
await _player!.setPlaylistMode(
nextLoop ? PlaylistMode.single : PlaylistMode.none,
);
_emitPlaybackPreferences();
},
);
}
if (_inFullscreen) {
return ColoredBox(
color: Colors.black,
@@ -495,7 +526,9 @@ class _PostMediaViewerState extends State<PostMediaViewer>
}
List<String> _buildVideoUrls(Post post) {
final list = List<String>.from(MediaUrlSelector.video(post));
final list = _isAudio(post)
? List<String>.from(MediaUrlSelector.audio(post))
: List<String>.from(MediaUrlSelector.video(post));
final cloudStreams = post.cloudLinks
.where((l) => l.isStreamable && l.directStreamUrl != null)
.map((l) => l.directStreamUrl!);
@@ -509,15 +542,16 @@ class _PostMediaViewerState extends State<PostMediaViewer>
return list;
}
bool _isAudio(Post post) => MediaUrlSelector.isAudio(post);
bool _isVideo(Post post) {
if (_isAudio(post)) return false;
if (post.cloudLinks.any((l) => l.isStreamable)) return true;
final value = '${post.fileType} ${post.fileUrl}'.toLowerCase();
return value.contains('video') ||
value.contains('.webm') ||
value.contains('.mp4') ||
value.contains('.mov');
return MediaUrlSelector.isVideo(post);
}
bool _isPlayableMedia(Post post) => _isVideo(post) || _isAudio(post);
bool _isSwf(Post post) {
final value =
'${post.fileType} ${post.fileUrl} ${post.sampleUrl} ${post.source ?? ''}'
@@ -997,6 +1031,455 @@ class _CloseVideoIntent extends Intent {
const _CloseVideoIntent();
}
class _AudioSurface extends StatefulWidget {
const _AudioSurface({
required this.player,
required this.post,
required this.muted,
required this.loopAudio,
this.initialVolume = 100.0,
this.onVolumeChanged,
required this.fullscreen,
required this.errorMessage,
required this.onRetry,
required this.onToggleMute,
required this.onToggleLoop,
});
final Player player;
final Post post;
final bool muted;
final bool loopAudio;
final double initialVolume;
final ValueChanged<double>? onVolumeChanged;
final bool fullscreen;
final String? errorMessage;
final VoidCallback onRetry;
final VoidCallback onToggleMute;
final VoidCallback onToggleLoop;
@override
State<_AudioSurface> createState() => _AudioSurfaceState();
}
class _AudioSurfaceState extends State<_AudioSurface>
with SingleTickerProviderStateMixin {
late final AnimationController _rotationController;
StreamSubscription<Duration>? _posSub;
StreamSubscription<Duration>? _durSub;
StreamSubscription<bool>? _playSub;
StreamSubscription<bool>? _buffSub;
Duration _position = Duration.zero;
Duration _duration = Duration.zero;
bool _playing = false;
bool _buffering = false;
bool _isDragging = false;
double _dragValue = 0.0;
@override
void initState() {
super.initState();
_rotationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 14),
);
_position = widget.player.state.position;
_duration = widget.player.state.duration;
_playing = widget.player.state.playing;
_buffering = widget.player.state.buffering;
if (_playing) _rotationController.repeat();
_posSub = widget.player.stream.position.listen((pos) {
if (mounted && !_isDragging) {
setState(() => _position = pos);
}
});
_durSub = widget.player.stream.duration.listen((dur) {
if (mounted) setState(() => _duration = dur);
});
_playSub = widget.player.stream.playing.listen((playing) {
if (mounted) {
setState(() => _playing = playing);
if (playing) {
_rotationController.repeat();
} else {
_rotationController.stop();
}
}
});
_buffSub = widget.player.stream.buffering.listen((buffering) {
if (mounted) setState(() => _buffering = buffering);
});
}
@override
void dispose() {
_rotationController.dispose();
_posSub?.cancel();
_durSub?.cancel();
_playSub?.cancel();
_buffSub?.cancel();
super.dispose();
}
String _formatTime(Duration d) {
final minutes = d.inMinutes;
final seconds = d.inSeconds % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
String _deriveTitle() {
final title = (widget.post.title ?? '').trim();
if (title.isNotEmpty) return title;
final uri = Uri.tryParse(widget.post.fileUrl);
if (uri != null) {
final q = uri.queryParameters['f'];
if (q != null && q.trim().isNotEmpty) return q.trim();
final seg = uri.pathSegments.lastOrNull;
if (seg != null && seg.isNotEmpty) return seg;
}
return 'Аудиозапись';
}
void _seekRelative(int seconds) {
final target = _position + Duration(seconds: seconds);
final maxDur = _duration > Duration.zero ? _duration : target;
final clamped = Duration(
milliseconds: target.inMilliseconds.clamp(0, maxDur.inMilliseconds),
);
widget.player.seek(clamped);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final title = _deriveTitle();
final artistName =
widget.post.tagGroups['artist']?.firstOrNull ?? widget.post.providerName;
final hasCover = widget.post.previewUrl.isNotEmpty &&
widget.post.previewUrl.startsWith('http');
if (widget.errorMessage != null) {
return Center(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: theme.colorScheme.error.withValues(alpha: 0.4)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline_rounded,
size: 40, color: theme.colorScheme.error),
const SizedBox(height: 12),
Text(
'Не удалось воспроизвести аудио',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
widget.errorMessage!,
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
FilledButton.icon(
onPressed: widget.onRetry,
icon: const Icon(Icons.refresh_rounded),
label: const Text('Повторить попытку'),
),
],
),
),
);
}
final maxMs = _duration.inMilliseconds > 0
? _duration.inMilliseconds.toDouble()
: 1.0;
final currentMs = (_isDragging ? _dragValue : _position.inMilliseconds.toDouble())
.clamp(0.0, maxMs);
return Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.surfaceContainerHighest,
theme.colorScheme.surfaceContainer,
],
),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
width: 0.8,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.25),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Top Row: Vinyl / Album Art + Title + Artist
Row(
children: [
// Rotating Album Art / Vinyl
RotationTransition(
turns: _rotationController,
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
theme.colorScheme.primary.withValues(alpha: 0.9),
Colors.black87,
],
),
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 0.35),
blurRadius: 12,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: hasCover
? ClipOval(
child: CachedNetworkImage(
imageUrl: widget.post.previewUrl,
width: 44,
height: 44,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => Icon(
Icons.audiotrack_rounded,
color: theme.colorScheme.onPrimary,
size: 26,
),
),
)
: Container(
width: 26,
height: 26,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: theme.colorScheme.surface,
),
child: Icon(
Icons.audiotrack_rounded,
size: 16,
color: theme.colorScheme.primary,
),
),
),
),
),
const SizedBox(width: 16),
// Track & Artist Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: -0.2,
height: 1.25,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.person_outline_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Expanded(
child: Text(
artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
],
),
const SizedBox(height: 18),
// Timeline Slider
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 4.5,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6.5),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 14),
activeTrackColor: theme.colorScheme.primary,
inactiveTrackColor:
theme.colorScheme.onSurface.withValues(alpha: 0.15),
thumbColor: theme.colorScheme.primary,
),
child: Slider(
value: currentMs,
min: 0.0,
max: maxMs,
onChanged: (val) {
setState(() {
_isDragging = true;
_dragValue = val;
});
},
onChangeEnd: (val) {
_isDragging = false;
widget.player.seek(Duration(milliseconds: val.toInt()));
},
),
),
// Time numbers: elapsed & total
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_formatTime(_position),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontSize: 11,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
if (_buffering)
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: theme.colorScheme.primary,
),
),
Text(
_formatTime(_duration),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontSize: 11,
fontFeatures: const [FontFeature.tabularFigures()],
),
),
],
),
),
const SizedBox(height: 10),
// Controls Row: Loop, -10s, Play/Pause, +10s, Mute
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
tooltip: widget.loopAudio ? 'Повтор: включен' : 'Повтор: выключен',
icon: Icon(
widget.loopAudio ? Icons.repeat_one_rounded : Icons.repeat_rounded,
color: widget.loopAudio
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant,
size: 22,
),
onPressed: widget.onToggleLoop,
),
IconButton(
tooltip: 'Назад на 10 сек',
icon: const Icon(Icons.replay_10_rounded, size: 26),
color: theme.colorScheme.onSurface,
onPressed: () => _seekRelative(-10),
),
// Big Play / Pause Button
GestureDetector(
onTap: () {
if (_playing) {
widget.player.pause();
} else {
widget.player.play();
}
},
child: Container(
width: 54,
height: 54,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 0.4),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: Icon(
_playing ? Icons.pause_rounded : Icons.play_arrow_rounded,
color: theme.colorScheme.onPrimary,
size: 30,
),
),
),
IconButton(
tooltip: 'Вперед на 10 сек',
icon: const Icon(Icons.forward_10_rounded, size: 26),
color: theme.colorScheme.onSurface,
onPressed: () => _seekRelative(10),
),
IconButton(
tooltip: widget.muted ? 'Включить звук' : 'Выключить звук',
icon: Icon(
widget.muted ? Icons.volume_off_rounded : Icons.volume_up_rounded,
color: widget.muted
? theme.colorScheme.error
: theme.colorScheme.onSurfaceVariant,
size: 22,
),
onPressed: widget.onToggleMute,
),
],
),
],
),
);
}
}
class _VideoSurface extends StatefulWidget {
const _VideoSurface({
required this.player,
@@ -288,14 +288,6 @@ class _ProviderFormScreenState extends ConsumerState<ProviderFormScreen> {
value: 'realbooru_html',
child: Text('Realbooru HTML'),
),
DropdownMenuItem(
value: 'kemono',
child: Text('Kemono Party artists'),
),
DropdownMenuItem(
value: 'coomer',
child: Text('Coomer Party artists'),
),
DropdownMenuItem(
value: 'custom',
child: Text('Custom REST API'),
@@ -305,16 +305,6 @@ class ProviderCard extends StatelessWidget {
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,
@@ -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,
+15 -8
View File
@@ -352,19 +352,23 @@ 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: 0, right: 2),
@@ -374,12 +378,15 @@ class _MobileShell extends StatelessWidget {
),
),
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,
),
);
}
}
+287 -5
View File
@@ -117,14 +117,19 @@ class _PostCardState extends ConsumerState<PostCard>
Widget build(BuildContext context) {
final post = _resolvedPost ?? widget.post;
final sensitive = _isSensitive(post.rating);
final isAudioPost =
MediaUrlSelector.isAudio(post) || post.fileType == 'audio';
final isVideoPost = MediaUrlSelector.isVideo(post);
final isTextPost = post.fileType == 'text' ||
(post.previewUrl.trim().isEmpty &&
post.sampleUrl.trim().isEmpty &&
post.fileUrl.trim().isEmpty &&
!MediaUrlSelector.isVideo(post));
!isVideoPost &&
!isAudioPost);
final aspect = post.width > 0 && post.height > 0
? post.width / post.height
: _fallbackAspect(isTextPost ? 'text' : post.fileType);
: _fallbackAspect(
isTextPost ? 'text' : (isAudioPost ? 'audio' : post.fileType));
final mobile = MediaQuery.sizeOf(context).width < 700;
final urls = _feedUrls(post, mobile: mobile);
final imageUrl = urls.isEmpty ? post.previewUrl : urls.first;
@@ -213,6 +218,18 @@ class _PostCardState extends ConsumerState<PostCard>
hovered: _hovered && !mobile,
);
}
if (isAudioPost && (imageUrl.isEmpty || !imageUrl.startsWith('http'))) {
return _AudioPostCardPreview(
post: post,
hovered: _hovered && !mobile,
);
}
if (isVideoPost && (imageUrl.isEmpty || !imageUrl.startsWith('http'))) {
return _VideoPostCardPreview(
post: post,
hovered: _hovered && !mobile,
);
}
final dpr = MediaQuery.devicePixelRatioOf(context);
final cacheWidth =
(constraints.maxWidth * dpr).round().clamp(320, 1800);
@@ -241,8 +258,14 @@ class _PostCardState extends ConsumerState<PostCard>
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
child: const Center(
child: Icon(Icons.broken_image_rounded),
child: Center(
child: Icon(
isVideoPost
? Icons.videocam_outlined
: (isAudioPost
? Icons.music_note_rounded
: Icons.broken_image_rounded),
),
),
),
);
@@ -282,12 +305,15 @@ class _PostCardState extends ConsumerState<PostCard>
post.fileType.toLowerCase().contains('webm') ||
post.fileType.toLowerCase().contains('mp4') ||
post.fileType.toLowerCase().contains('gif') ||
isAudioPost ||
isTextPost)
Positioned(
right: 8,
top: 8,
child: _MediaBadge(
fileType: isTextPost ? 'text' : post.fileType,
fileType: isAudioPost
? 'audio'
: (isTextPost ? 'text' : post.fileType),
),
),
Positioned(
@@ -450,6 +476,7 @@ class _PostCardState extends ConsumerState<PostCard>
double _fallbackAspect(String fileType) {
final normalized = fileType.toLowerCase();
if (normalized == 'text') return 0.95;
if (normalized == 'audio' || normalized.contains('mp3')) return 1.25;
if (normalized.contains('video') || normalized.contains('webm')) {
return 16 / 9;
}
@@ -806,6 +833,14 @@ class _MediaBadge extends StatelessWidget {
String _type(String raw) {
final value = raw.toLowerCase();
if (value.contains('audio') ||
value.contains('mp3') ||
value.contains('wav') ||
value.contains('flac') ||
value.contains('ogg') ||
value.contains('m4a')) {
return 'audio';
}
if (value.contains('video') ||
value.contains('webm') ||
value.contains('mp4')) {
@@ -817,6 +852,7 @@ class _MediaBadge extends StatelessWidget {
}
IconData _icon(String type) => switch (type) {
'audio' => Icons.music_note_rounded,
'video' => Icons.play_arrow_rounded,
'gif' => Icons.gif_box_rounded,
'text' => Icons.article_rounded,
@@ -824,6 +860,7 @@ class _MediaBadge extends StatelessWidget {
};
String _label(String type) => switch (type) {
'audio' => 'AUDIO',
'video' => 'VIDEO',
'gif' => 'GIF',
'text' => 'ТЕКСТ',
@@ -967,6 +1004,251 @@ class _TextPostCardPreview extends StatelessWidget {
}
}
class _AudioPostCardPreview extends StatelessWidget {
const _AudioPostCardPreview({
required this.post,
required this.hovered,
});
final Post post;
final bool hovered;
String _deriveAudioTitle() {
final title = (post.title ?? '').trim();
if (title.isNotEmpty) return title;
final uri = Uri.tryParse(post.fileUrl);
if (uri != null) {
final queryParam = uri.queryParameters['f'];
if (queryParam != null && queryParam.trim().isNotEmpty) {
return queryParam.trim();
}
final lastSeg = uri.pathSegments.lastOrNull;
if (lastSeg != null && lastSeg.isNotEmpty) {
return lastSeg;
}
}
final artist = post.tagGroups['artist']?.firstOrNull;
if (artist != null && artist.isNotEmpty) return 'Аудиозапись от $artist';
return 'Аудиозапись';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final audioTitle = _deriveAudioTitle();
final artistName = post.tagGroups['artist']?.firstOrNull ?? post.providerName;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.primaryContainer.withValues(alpha: 0.85),
theme.colorScheme.surfaceContainerHighest,
],
),
border: Border.all(
color: hovered
? theme.colorScheme.primary.withValues(alpha: 0.6)
: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.audiotrack_rounded,
size: 20,
color: theme.colorScheme.onPrimary,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: theme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
Text(
'${post.createdAt.day.toString().padLeft(2, '0')}.${post.createdAt.month.toString().padLeft(2, '0')}.${post.createdAt.year}',
style: TextStyle(
fontSize: 10,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
),
],
),
const Spacer(),
Text(
audioTitle,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
height: 1.25,
letterSpacing: -0.1,
),
),
const SizedBox(height: 10),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_filled_rounded,
size: 14,
color: theme.colorScheme.primary,
),
const SizedBox(width: 4),
Text(
'Воспроизвести',
style: TextStyle(
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
],
),
),
],
),
],
),
);
}
}
class _VideoPostCardPreview extends StatelessWidget {
const _VideoPostCardPreview({
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 artistName = post.tagGroups['artist']?.firstOrNull ?? post.providerName;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.surfaceContainerHighest,
theme.colorScheme.surfaceContainer,
],
),
border: Border.all(
color: hovered
? theme.colorScheme.primary.withValues(alpha: 0.6)
: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
width: 1,
),
),
child: Stack(
children: [
Center(
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.85),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 10,
offset: const Offset(0, 3),
),
],
),
child: const Icon(
Icons.play_arrow_rounded,
size: 32,
color: Colors.white,
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (title.isNotEmpty)
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
height: 1.2,
),
)
else
Text(
artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
}
}
class _SeenBadge extends StatelessWidget {
const _SeenBadge();
+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()
+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.
}
}
+126 -110
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,10 +907,10 @@ 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:
@@ -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 -1
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.4.1+75
version: 3.5.4+80
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
+88
View File
@@ -1,8 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:gel_rule_app/backend/models/artist_work_query.dart';
import 'package:gel_rule_app/backend/models/content_provider_config.dart';
import 'package:gel_rule_app/backend/models/post.dart';
import 'package:gel_rule_app/backend/providers/pawchive_provider.dart';
import 'package:gel_rule_app/backend/providers/provider_factory.dart';
import 'package:gel_rule_app/backend/utils/media_quality.dart';
import 'package:gel_rule_app/core/http/dio_client.dart';
void main() {
@@ -47,4 +49,90 @@ void main() {
expect(provider.id, query.providerId);
expect(query.artistName, 'BOKABA');
});
test('MediaUrlSelector accurately classifies audio, video and photo posts', () {
final audioPost = Post(
id: 'patreon:123:456:0',
providerId: 'pawchive',
providerName: 'Pawchive',
previewUrl: '',
sampleUrl: '',
fileUrl:
'https://file.pawchive.pw/data/ae/13/ae13adaaebee798397c5c3b07cb9690b2adc91f41815e89a3f58a447bfb56980.mp3?f=audio%20spicy%20only%204.MP3',
tags: const ['patreon', 'artist', 'audio'],
rating: 'unknown',
width: 0,
height: 0,
createdAt: DateTime.now(),
fileType: 'audio',
score: 0,
);
final videoPost = Post(
id: 'patreon:123:456:1',
providerId: 'pawchive',
providerName: 'Pawchive',
previewUrl: '',
sampleUrl: '',
fileUrl:
'https://file.pawchive.pw/data/84/b1/84b1d076dd6b59047bb699115e57c18a02f2ea08b54e4d20409692b58b7aac33.mp4',
tags: const ['patreon', 'artist'],
rating: 'unknown',
width: 1920,
height: 1080,
createdAt: DateTime.now(),
fileType: 'video',
score: 0,
);
final photoPost = Post(
id: 'patreon:123:456:2',
providerId: 'pawchive',
providerName: 'Pawchive',
previewUrl: 'https://img.pawchive.pw/thumbnail/data/c3/06/c3060ab6fa.png',
sampleUrl: 'https://img.pawchive.pw/thumbnail/data/c3/06/c3060ab6fa.png',
fileUrl: 'https://file.pawchive.pw/data/c3/06/c3060ab6fa.png',
tags: const ['patreon', 'artist'],
rating: 'unknown',
width: 1200,
height: 1600,
createdAt: DateTime.now(),
fileType: 'photo',
score: 0,
);
expect(MediaUrlSelector.isAudio(audioPost), isTrue);
expect(MediaUrlSelector.isVideo(audioPost), isFalse);
expect(MediaUrlSelector.audio(audioPost), contains(audioPost.fileUrl));
expect(MediaUrlSelector.isVideo(videoPost), isTrue);
expect(MediaUrlSelector.isAudio(videoPost), isFalse);
expect(MediaUrlSelector.isAudio(photoPost), isFalse);
expect(MediaUrlSelector.isVideo(photoPost), isFalse);
});
test('MediaUrlSelector classifies mp4 containing "audio" in query parameter as video and not audio', () {
final trickyVideoPost = Post(
id: 'patreon:123:456:3',
providerId: 'pawchive',
providerName: 'Pawchive',
previewUrl: '',
sampleUrl: '',
fileUrl:
'https://file.pawchive.pw/data/b4/65/b465cb03ab7b26c0a677db3ae43500d1ef71e3e25151f160cda244c1fb40cfae.mp4?f=nsfw%202%20-%20audio%20spicy.mp4',
tags: const ['patreon', 'artist', 'nsfw 2 - audio spicy'],
rating: 'unknown',
width: 1920,
height: 1080,
createdAt: DateTime.now(),
fileType: 'video',
score: 0,
);
expect(MediaUrlSelector.isVideo(trickyVideoPost), isTrue);
expect(MediaUrlSelector.isAudio(trickyVideoPost), isFalse);
expect(MediaUrlSelector.video(trickyVideoPost), contains(trickyVideoPost.fileUrl));
});
}
+2 -3
View File
@@ -6,7 +6,6 @@ import 'package:gel_rule_app/backend/mappers/moebooru_mapper.dart';
import 'package:gel_rule_app/backend/mappers/rule34_mapper.dart';
import 'package:gel_rule_app/backend/models/content_provider_config.dart';
import 'package:gel_rule_app/backend/providers/custom_provider.dart';
import 'package:gel_rule_app/backend/providers/kemono_provider.dart';
import 'package:gel_rule_app/backend/providers/provider_factory.dart';
import 'package:gel_rule_app/backend/providers/realbooru_html_provider.dart';
import 'package:gel_rule_app/backend/repositories/provider_repository.dart';
@@ -307,7 +306,7 @@ void main() {
expect(provider, isA<RealbooruHtmlProvider>());
});
test('provider factory creates Kemono and Coomer artist providers', () {
test('provider factory treats removed Kemono and Coomer as unsupported', () {
final now = DateTime(2026);
for (final apiType in ['kemono', 'coomer']) {
final provider = ProviderFactory().create(ContentProviderConfig(
@@ -323,7 +322,7 @@ void main() {
updatedAt: now,
));
expect(provider, isA<KemonoProvider>());
expect(provider, isA<UnsupportedCustomProvider>());
}
});
}