Rebrand to Lunaris and add CosBooru providers
This commit is contained in:
@@ -1,31 +1,32 @@
|
|||||||
# RuleGel
|
# Lunaris
|
||||||
|
|
||||||
RuleGel — единое Flutter-приложение для просмотра booru/gelbooru/rule34-подобных источников в Pinterest-стиле.
|
Lunaris — локальное Flutter-приложение для просмотра booru/gelbooru/rule34-подобных источников в Pinterest-стиле.
|
||||||
|
|
||||||
Приложение не использует отдельный backend-сервер. Вся логика работает локально внутри Flutter: провайдеры API, кэш metadata, избранное, коллекции, история, настройки, диагностика и фильтры.
|
Приложение не использует отдельный backend-сервер. Вся логика работает внутри Flutter: API-провайдеры, metadata-кэш, избранное, коллекции, история, настройки, диагностика и фильтры.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- Pinterest-like masonry feed с несколькими провайдерами.
|
- Masonry feed с несколькими провайдерами и перемешанной выдачей All providers.
|
||||||
- Поиск по тегам через удобные chip-теги: ввёл тег, нажал пробел или Enter, тег стал отдельным элементом.
|
- Поиск по тегам через chip-input: ввёл тег, нажал пробел или Enter, тег стал отдельным элементом.
|
||||||
- Подсказки тегов из provider API.
|
- Подсказки тегов из provider API.
|
||||||
- Избранное, коллекции и история просмотренных постов.
|
- Избранное, коллекции и история просмотренных постов.
|
||||||
- Smart blacklist и whitelist с правилами по тегам, рейтингу, провайдеру, типу файла и score.
|
- Smart blacklist/whitelist с правилами по тегам, рейтингу, провайдеру, типу файла и score.
|
||||||
- Детальный просмотр фото, GIF и видео.
|
- Детальный просмотр фото, GIF, видео и SWF на Windows.
|
||||||
- Видео-плеер с fullscreen, repeat, mute и понижением громкости.
|
- Видео-плеер с fullscreen, repeat, mute, half-volume и сохранением выбранных настроек.
|
||||||
- Desktop-only просмотр `.swf` через Ruffle/WebView на Windows.
|
|
||||||
- Управление провайдерами, health-check и provider diagnostics.
|
- Управление провайдерами, health-check и provider diagnostics.
|
||||||
- Экспорт и импорт настроек.
|
- Экспорт и импорт настроек.
|
||||||
|
- Автопроверка обновлений через Gitea releases.
|
||||||
- Android и Windows сборки.
|
- Android и Windows сборки.
|
||||||
|
|
||||||
## Скачать
|
## Скачать
|
||||||
|
|
||||||
Готовые сборки лежат в Gitea Releases:
|
Готовые сборки лежат в Gitea Releases:
|
||||||
|
|
||||||
[RuleGel v0.3.0](https://gitea.rardogsynapse.online/RarDog/RuleGelApp/releases/tag/v0.3.0-tag-chips-swf)
|
[Lunaris v1.1.0](https://gitea.rardogsynapse.online/RarDog/RuleGelApp/releases/tag/v1.1.0)
|
||||||
|
|
||||||
- `RuleGelSetup.exe` — установщик для Windows.
|
- `Lunaris-v1.1.0.apk` — APK для Android.
|
||||||
- `RuleGel-v0.3.0.apk` — APK для Android.
|
- `LunarisSetup-v1.1.0.exe` — установщик для Windows.
|
||||||
|
- `LunarisPortable-v1.1.0.zip` — portable-версия для Windows.
|
||||||
|
|
||||||
## Стек
|
## Стек
|
||||||
|
|
||||||
@@ -39,11 +40,10 @@ RuleGel — единое Flutter-приложение для просмотра
|
|||||||
- media_kit
|
- media_kit
|
||||||
- webview_windows для SWF/Ruffle на Windows
|
- webview_windows для SWF/Ruffle на Windows
|
||||||
- connectivity_plus
|
- connectivity_plus
|
||||||
- freezed/json_serializable/build_runner
|
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
RuleGel — Flutter-монолит. UI не дублирует backend-логику, а вызывает локальные сервисы через Riverpod.
|
Lunaris — Flutter-монолит. UI не дублирует backend-логику, а вызывает локальные сервисы через Riverpod.
|
||||||
|
|
||||||
Основные слои:
|
Основные слои:
|
||||||
|
|
||||||
@@ -80,16 +80,21 @@ RuleGel — Flutter-монолит. UI не дублирует backend-логи
|
|||||||
- Yande.re — `https://yande.re`
|
- Yande.re — `https://yande.re`
|
||||||
- e621 — `https://e621.net`
|
- e621 — `https://e621.net`
|
||||||
- e926 — `https://e926.net`
|
- e926 — `https://e926.net`
|
||||||
|
- Xbooru — `https://xbooru.com`
|
||||||
|
- CosBooru — `https://cos.lycore.co`
|
||||||
|
|
||||||
|
Realbooru оставлен в списке провайдеров, но выключен по умолчанию, потому что его публичный API сейчас нестабилен.
|
||||||
|
|
||||||
Поддерживаемые `apiType`:
|
Поддерживаемые `apiType`:
|
||||||
|
|
||||||
- `gelbooru`
|
- `gelbooru`
|
||||||
- `rule34`
|
- `rule34`
|
||||||
|
- `realbooru`
|
||||||
- `danbooru`
|
- `danbooru`
|
||||||
- `moebooru`
|
- `moebooru`
|
||||||
- `e621`
|
- `e621`
|
||||||
|
|
||||||
Один упавший провайдер не ломает общий feed: ошибки сохраняются в diagnostics, а остальные провайдеры продолжают отдавать посты.
|
Один упавший провайдер не ломает общий feed: ошибки сохраняются в diagnostics, остальные провайдеры продолжают отдавать посты.
|
||||||
|
|
||||||
## Как добавить провайдера
|
## Как добавить провайдера
|
||||||
|
|
||||||
@@ -133,37 +138,6 @@ Isar хранит:
|
|||||||
|
|
||||||
Кэш хранит только metadata. Оригинальные медиафайлы не скачиваются автоматически.
|
Кэш хранит только metadata. Оригинальные медиафайлы не скачиваются автоматически.
|
||||||
|
|
||||||
## Использование сервисов из UI
|
|
||||||
|
|
||||||
UI получает сервисы через Riverpod из `lib/backend/di/backend_providers.dart`.
|
|
||||||
|
|
||||||
Пример feed:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final result = await ref.read(feedServiceProvider).refresh(
|
|
||||||
tags: ['touhou', 'hakurei_reimu'],
|
|
||||||
rating: 'safe',
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Пример избранного:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
await ref.read(favoriteServiceProvider).addFavorite(post);
|
|
||||||
|
|
||||||
final saved = await ref
|
|
||||||
.read(favoriteServiceProvider)
|
|
||||||
.isFavorite(post.id, post.providerId);
|
|
||||||
```
|
|
||||||
|
|
||||||
Пример настроек:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
final settings = ref.read(settingsServiceProvider);
|
|
||||||
final exported = await settings.exportSettingsToJson();
|
|
||||||
await settings.importSettingsFromJson(exported.data);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Сборка и запуск
|
## Сборка и запуск
|
||||||
|
|
||||||
Установить зависимости:
|
Установить зависимости:
|
||||||
@@ -172,12 +146,6 @@ await settings.importSettingsFromJson(exported.data);
|
|||||||
flutter pub get
|
flutter pub get
|
||||||
```
|
```
|
||||||
|
|
||||||
Сгенерировать Isar-код:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dart run build_runner build --delete-conflicting-outputs
|
|
||||||
```
|
|
||||||
|
|
||||||
Запустить:
|
Запустить:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -195,7 +163,7 @@ flutter build apk
|
|||||||
Собрать Windows installer:
|
Собрать Windows installer:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" installer\RuleGel.iss
|
& "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe" installer\Lunaris.iss
|
||||||
```
|
```
|
||||||
|
|
||||||
## Важные заметки
|
## Важные заметки
|
||||||
@@ -203,13 +171,13 @@ flutter build apk
|
|||||||
- На Windows для Flutter plugins может понадобиться Developer Mode.
|
- На Windows для Flutter plugins может понадобиться Developer Mode.
|
||||||
- Android-сборка требует Android SDK и принятые `flutter doctor --android-licenses`.
|
- Android-сборка требует Android SDK и принятые `flutter doctor --android-licenses`.
|
||||||
- Linux build нужно собирать на Linux-хосте.
|
- Linux build нужно собирать на Linux-хосте.
|
||||||
- Если путь проекта содержит апостроф, Flutter Windows/test tooling может капризничать. Для сборки можно использовать junction-путь без спецсимволов.
|
- Если путь проекта содержит апостроф, Flutter Windows/test tooling может ломаться. Для сборки можно использовать копию проекта в пути без спецсимволов.
|
||||||
- SWF работает только на Windows. На Android он намеренно отключён.
|
- SWF работает только на Windows через Ruffle/WebView. На Android он намеренно отключён.
|
||||||
- Для SWF на Windows может понадобиться Microsoft WebView2 Runtime.
|
- Для SWF на Windows может понадобиться Microsoft WebView2 Runtime.
|
||||||
|
|
||||||
## Безопасность
|
## Безопасность
|
||||||
|
|
||||||
- RuleGel использует публичные API и metadata.
|
- Lunaris использует публичные API и metadata.
|
||||||
- Приложение не обходит ограничения сайтов.
|
- Приложение не обходит ограничения сайтов.
|
||||||
- Оригинальные файлы скачиваются только вручную по действию пользователя.
|
- Оригинальные файлы скачиваются только вручную по действию пользователя.
|
||||||
- Dio использует нормальный User-Agent.
|
- Dio использует нормальный User-Agent.
|
||||||
@@ -223,4 +191,4 @@ flutter analyze
|
|||||||
flutter test
|
flutter test
|
||||||
```
|
```
|
||||||
|
|
||||||
В текущей версии покрыты backend-сервисы, provider parsing, smart blacklist, viewed history и tag chip input.
|
Покрыты backend-сервисы, provider parsing, smart blacklist, viewed history, settings migration и tag chip input.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="RuleGel"
|
android:label="Lunaris"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "RuleGel"
|
#define MyAppName "Lunaris"
|
||||||
#define MyAppVersion "1.0.2"
|
#define MyAppVersion "1.1.0"
|
||||||
#define MyAppPublisher "RarDog"
|
#define MyAppPublisher "RarDog"
|
||||||
#define MyAppExeName "gel_rule_app.exe"
|
#define MyAppExeName "gel_rule_app.exe"
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ DefaultGroupName={#MyAppName}
|
|||||||
DisableProgramGroupPage=yes
|
DisableProgramGroupPage=yes
|
||||||
PrivilegesRequired=lowest
|
PrivilegesRequired=lowest
|
||||||
OutputDir=..\dist
|
OutputDir=..\dist
|
||||||
OutputBaseFilename=RuleGelSetup
|
OutputBaseFilename=LunarisSetup
|
||||||
SetupIconFile=..\windows\runner\resources\app_icon.ico
|
SetupIconFile=..\windows\runner\resources\app_icon.ico
|
||||||
Compression=lzma2
|
Compression=lzma2
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
+2
-2
@@ -32,7 +32,7 @@ class GelRuleApp extends ConsumerWidget {
|
|||||||
home: _StartupErrorScreen(error: error),
|
home: _StartupErrorScreen(error: error),
|
||||||
),
|
),
|
||||||
data: (_) => MaterialApp.router(
|
data: (_) => MaterialApp.router(
|
||||||
title: 'RuleGel',
|
title: 'Lunaris',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: buildTheme(Brightness.light),
|
theme: buildTheme(Brightness.light),
|
||||||
darkTheme: buildTheme(Brightness.dark),
|
darkTheme: buildTheme(Brightness.dark),
|
||||||
@@ -79,7 +79,7 @@ class _AppOverlayState extends ConsumerState<_AppOverlay> {
|
|||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text('RuleGel ${info.version} is available'),
|
title: Text('Lunaris ${info.version} is available'),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 520),
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
const appDisplayVersion = '1.0.2';
|
const appDisplayVersion = '1.1.0';
|
||||||
const appBuildNumber = 10;
|
const appBuildNumber = 11;
|
||||||
|
|||||||
+18
-7
@@ -1,5 +1,16 @@
|
|||||||
const ruleGelChangelog = [
|
const lunarisChangelog = [
|
||||||
RuleGelChange(
|
LunarisChange(
|
||||||
|
version: '1.1.0',
|
||||||
|
title: 'Lunaris rebrand and CosBooru',
|
||||||
|
bullets: [
|
||||||
|
'RuleGel is now Lunaris while keeping the same package identity for updates.',
|
||||||
|
'CosBooru now uses the working Danbooru-compatible API at cos.lycore.co.',
|
||||||
|
'Xbooru was added as a default Gelbooru-compatible provider.',
|
||||||
|
'Rule34 Paheal was removed from providers.',
|
||||||
|
'Video player UI preferences are saved across fullscreen, details and restarts.',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
LunarisChange(
|
||||||
version: '1.0.2',
|
version: '1.0.2',
|
||||||
title: 'Realbooru safety fix',
|
title: 'Realbooru safety fix',
|
||||||
bullets: [
|
bullets: [
|
||||||
@@ -8,7 +19,7 @@ const ruleGelChangelog = [
|
|||||||
'Existing enabled Realbooru configs are disabled on startup so the main feed is not polluted by a known-offline API.',
|
'Existing enabled Realbooru configs are disabled on startup so the main feed is not polluted by a known-offline API.',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
RuleGelChange(
|
LunarisChange(
|
||||||
version: '1.0.1',
|
version: '1.0.1',
|
||||||
title: 'Realbooru and mobile polish',
|
title: 'Realbooru and mobile polish',
|
||||||
bullets: [
|
bullets: [
|
||||||
@@ -18,7 +29,7 @@ const ruleGelChangelog = [
|
|||||||
'Tag chip input no longer jumps while typing.',
|
'Tag chip input no longer jumps while typing.',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
RuleGelChange(
|
LunarisChange(
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
title: 'Stable 1.0',
|
title: 'Stable 1.0',
|
||||||
bullets: [
|
bullets: [
|
||||||
@@ -28,7 +39,7 @@ const ruleGelChangelog = [
|
|||||||
'Windows portable package is published alongside APK and installer.',
|
'Windows portable package is published alongside APK and installer.',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
RuleGelChange(
|
LunarisChange(
|
||||||
version: '0.4.3',
|
version: '0.4.3',
|
||||||
title: 'Bugfixes',
|
title: 'Bugfixes',
|
||||||
bullets: [
|
bullets: [
|
||||||
@@ -40,8 +51,8 @@ const ruleGelChangelog = [
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
class RuleGelChange {
|
class LunarisChange {
|
||||||
const RuleGelChange({
|
const LunarisChange({
|
||||||
required this.version,
|
required this.version,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.bullets,
|
required this.bullets,
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ final updateServiceProvider = Provider<UpdateService>((ref) {
|
|||||||
connectTimeout: const Duration(seconds: 12),
|
connectTimeout: const Duration(seconds: 12),
|
||||||
receiveTimeout: const Duration(seconds: 12),
|
receiveTimeout: const Duration(seconds: 12),
|
||||||
headers: const {
|
headers: const {
|
||||||
'User-Agent': 'RuleGel/1.0 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class ProviderRepository {
|
|||||||
priority: 5,
|
priority: 5,
|
||||||
timeoutSeconds: 20,
|
timeoutSeconds: 20,
|
||||||
customHeaders: const {
|
customHeaders: const {
|
||||||
'User-Agent': 'RuleGel/0.2 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
},
|
},
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -102,7 +102,7 @@ class ProviderRepository {
|
|||||||
priority: 6,
|
priority: 6,
|
||||||
timeoutSeconds: 20,
|
timeoutSeconds: 20,
|
||||||
customHeaders: const {
|
customHeaders: const {
|
||||||
'User-Agent': 'RuleGel/0.2 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
},
|
},
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -119,6 +119,30 @@ class ProviderRepository {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
),
|
),
|
||||||
|
ContentProviderConfig(
|
||||||
|
id: 'xbooru',
|
||||||
|
name: 'Xbooru',
|
||||||
|
baseUrl: 'https://xbooru.com',
|
||||||
|
apiType: 'gelbooru',
|
||||||
|
enabled: true,
|
||||||
|
priority: 8,
|
||||||
|
timeoutSeconds: 20,
|
||||||
|
customHeaders: const {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
ContentProviderConfig(
|
||||||
|
id: 'cosbooru',
|
||||||
|
name: 'CosBooru',
|
||||||
|
baseUrl: 'https://cos.lycore.co',
|
||||||
|
apiType: 'danbooru',
|
||||||
|
enabled: true,
|
||||||
|
priority: 9,
|
||||||
|
timeoutSeconds: 20,
|
||||||
|
customHeaders: const {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,10 +152,35 @@ class ProviderRepository {
|
|||||||
await isar.providerConfigEntitys.where().findAll();
|
await isar.providerConfigEntitys.where().findAll();
|
||||||
for (final provider in existingProviders) {
|
for (final provider in existingProviders) {
|
||||||
if (provider.providerId == 'kemono' ||
|
if (provider.providerId == 'kemono' ||
|
||||||
provider.providerId == 'coomer') {
|
provider.providerId == 'coomer' ||
|
||||||
|
provider.providerId == 'paheal') {
|
||||||
await isar.providerConfigEntitys.delete(provider.isarId);
|
await isar.providerConfigEntitys.delete(provider.isarId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await isar.providerHealthEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
|
await isar.providerDiagnosticsEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
|
await isar.cachedPostEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
|
await isar.favoriteEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
|
await isar.collectionPostEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
|
await isar.viewedPostEntitys
|
||||||
|
.filter()
|
||||||
|
.providerIdEqualTo('paheal')
|
||||||
|
.deleteAll();
|
||||||
final seeds = seedProviders();
|
final seeds = seedProviders();
|
||||||
for (final seed in seeds) {
|
for (final seed in seeds) {
|
||||||
final exists = await isar.providerConfigEntitys
|
final exists = await isar.providerConfigEntitys
|
||||||
@@ -145,6 +194,17 @@ class ProviderRepository {
|
|||||||
exists.enabled = false;
|
exists.enabled = false;
|
||||||
exists.updatedAt = DateTime.now();
|
exists.updatedAt = DateTime.now();
|
||||||
await isar.providerConfigEntitys.put(exists);
|
await isar.providerConfigEntitys.put(exists);
|
||||||
|
} else if (seed.id == 'cosbooru' &&
|
||||||
|
(exists.baseUrl != seed.baseUrl ||
|
||||||
|
exists.apiType != seed.apiType)) {
|
||||||
|
exists.name = seed.name;
|
||||||
|
exists.baseUrl = seed.baseUrl;
|
||||||
|
exists.apiType = seed.apiType;
|
||||||
|
exists.enabled = seed.enabled;
|
||||||
|
exists.priority = seed.priority;
|
||||||
|
exists.timeoutSeconds = seed.timeoutSeconds;
|
||||||
|
exists.updatedAt = DateTime.now();
|
||||||
|
await isar.providerConfigEntitys.put(exists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class AppSettings {
|
|||||||
required this.mediaQualityMode,
|
required this.mediaQualityMode,
|
||||||
required this.motionRefreshMode,
|
required this.motionRefreshMode,
|
||||||
required this.autoBatterySaver60Hz,
|
required this.autoBatterySaver60Hz,
|
||||||
|
required this.videoPlayerMuted,
|
||||||
|
required this.videoPlayerHalfVolume,
|
||||||
|
required this.videoPlayerLoop,
|
||||||
|
required this.videoPlayerCover,
|
||||||
required this.hiddenPostKeys,
|
required this.hiddenPostKeys,
|
||||||
required this.diagnosticLogLines,
|
required this.diagnosticLogLines,
|
||||||
required this.lastFeedTags,
|
required this.lastFeedTags,
|
||||||
@@ -64,6 +68,10 @@ class AppSettings {
|
|||||||
final String mediaQualityMode;
|
final String mediaQualityMode;
|
||||||
final String motionRefreshMode;
|
final String motionRefreshMode;
|
||||||
final bool autoBatterySaver60Hz;
|
final bool autoBatterySaver60Hz;
|
||||||
|
final bool videoPlayerMuted;
|
||||||
|
final bool videoPlayerHalfVolume;
|
||||||
|
final bool videoPlayerLoop;
|
||||||
|
final bool videoPlayerCover;
|
||||||
final List<String> hiddenPostKeys;
|
final List<String> hiddenPostKeys;
|
||||||
final List<String> diagnosticLogLines;
|
final List<String> diagnosticLogLines;
|
||||||
final List<String> lastFeedTags;
|
final List<String> lastFeedTags;
|
||||||
@@ -96,6 +104,10 @@ class AppSettings {
|
|||||||
mediaQualityMode: 'auto',
|
mediaQualityMode: 'auto',
|
||||||
motionRefreshMode: 'auto',
|
motionRefreshMode: 'auto',
|
||||||
autoBatterySaver60Hz: true,
|
autoBatterySaver60Hz: true,
|
||||||
|
videoPlayerMuted: false,
|
||||||
|
videoPlayerHalfVolume: false,
|
||||||
|
videoPlayerLoop: false,
|
||||||
|
videoPlayerCover: false,
|
||||||
hiddenPostKeys: [],
|
hiddenPostKeys: [],
|
||||||
diagnosticLogLines: [],
|
diagnosticLogLines: [],
|
||||||
lastFeedTags: [],
|
lastFeedTags: [],
|
||||||
@@ -129,6 +141,10 @@ class AppSettings {
|
|||||||
String? mediaQualityMode,
|
String? mediaQualityMode,
|
||||||
String? motionRefreshMode,
|
String? motionRefreshMode,
|
||||||
bool? autoBatterySaver60Hz,
|
bool? autoBatterySaver60Hz,
|
||||||
|
bool? videoPlayerMuted,
|
||||||
|
bool? videoPlayerHalfVolume,
|
||||||
|
bool? videoPlayerLoop,
|
||||||
|
bool? videoPlayerCover,
|
||||||
List<String>? hiddenPostKeys,
|
List<String>? hiddenPostKeys,
|
||||||
List<String>? diagnosticLogLines,
|
List<String>? diagnosticLogLines,
|
||||||
List<String>? lastFeedTags,
|
List<String>? lastFeedTags,
|
||||||
@@ -164,6 +180,11 @@ class AppSettings {
|
|||||||
mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode,
|
mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode,
|
||||||
motionRefreshMode: motionRefreshMode ?? this.motionRefreshMode,
|
motionRefreshMode: motionRefreshMode ?? this.motionRefreshMode,
|
||||||
autoBatterySaver60Hz: autoBatterySaver60Hz ?? this.autoBatterySaver60Hz,
|
autoBatterySaver60Hz: autoBatterySaver60Hz ?? this.autoBatterySaver60Hz,
|
||||||
|
videoPlayerMuted: videoPlayerMuted ?? this.videoPlayerMuted,
|
||||||
|
videoPlayerHalfVolume:
|
||||||
|
videoPlayerHalfVolume ?? this.videoPlayerHalfVolume,
|
||||||
|
videoPlayerLoop: videoPlayerLoop ?? this.videoPlayerLoop,
|
||||||
|
videoPlayerCover: videoPlayerCover ?? this.videoPlayerCover,
|
||||||
hiddenPostKeys: hiddenPostKeys ?? this.hiddenPostKeys,
|
hiddenPostKeys: hiddenPostKeys ?? this.hiddenPostKeys,
|
||||||
diagnosticLogLines: diagnosticLogLines ?? this.diagnosticLogLines,
|
diagnosticLogLines: diagnosticLogLines ?? this.diagnosticLogLines,
|
||||||
lastFeedTags: lastFeedTags ?? this.lastFeedTags,
|
lastFeedTags: lastFeedTags ?? this.lastFeedTags,
|
||||||
@@ -199,6 +220,10 @@ class AppSettings {
|
|||||||
'mediaQualityMode': mediaQualityMode,
|
'mediaQualityMode': mediaQualityMode,
|
||||||
'motionRefreshMode': motionRefreshMode,
|
'motionRefreshMode': motionRefreshMode,
|
||||||
'autoBatterySaver60Hz': autoBatterySaver60Hz,
|
'autoBatterySaver60Hz': autoBatterySaver60Hz,
|
||||||
|
'videoPlayerMuted': videoPlayerMuted,
|
||||||
|
'videoPlayerHalfVolume': videoPlayerHalfVolume,
|
||||||
|
'videoPlayerLoop': videoPlayerLoop,
|
||||||
|
'videoPlayerCover': videoPlayerCover,
|
||||||
'hiddenPostKeys': hiddenPostKeys,
|
'hiddenPostKeys': hiddenPostKeys,
|
||||||
'diagnosticLogLines': diagnosticLogLines,
|
'diagnosticLogLines': diagnosticLogLines,
|
||||||
'lastFeedTags': lastFeedTags,
|
'lastFeedTags': lastFeedTags,
|
||||||
@@ -259,6 +284,14 @@ class AppSettings {
|
|||||||
defaults.motionRefreshMode,
|
defaults.motionRefreshMode,
|
||||||
autoBatterySaver60Hz: (json['autoBatterySaver60Hz'] as bool?) ??
|
autoBatterySaver60Hz: (json['autoBatterySaver60Hz'] as bool?) ??
|
||||||
defaults.autoBatterySaver60Hz,
|
defaults.autoBatterySaver60Hz,
|
||||||
|
videoPlayerMuted:
|
||||||
|
(json['videoPlayerMuted'] as bool?) ?? defaults.videoPlayerMuted,
|
||||||
|
videoPlayerHalfVolume: (json['videoPlayerHalfVolume'] as bool?) ??
|
||||||
|
defaults.videoPlayerHalfVolume,
|
||||||
|
videoPlayerLoop:
|
||||||
|
(json['videoPlayerLoop'] as bool?) ?? defaults.videoPlayerLoop,
|
||||||
|
videoPlayerCover:
|
||||||
|
(json['videoPlayerCover'] as bool?) ?? defaults.videoPlayerCover,
|
||||||
hiddenPostKeys: List<String>.from(
|
hiddenPostKeys: List<String>.from(
|
||||||
(json['hiddenPostKeys'] as List?) ?? defaults.hiddenPostKeys,
|
(json['hiddenPostKeys'] as List?) ?? defaults.hiddenPostKeys,
|
||||||
),
|
),
|
||||||
@@ -448,7 +481,7 @@ class SettingsService {
|
|||||||
return Success(
|
return Success(
|
||||||
const JsonEncoder.withIndent(' ').convert({
|
const JsonEncoder.withIndent(' ').convert({
|
||||||
'app': {
|
'app': {
|
||||||
'name': 'RuleGel',
|
'name': 'Lunaris',
|
||||||
'version': appVersion,
|
'version': appVersion,
|
||||||
'build': buildNumber,
|
'build': buildNumber,
|
||||||
'generatedAt': DateTime.now().toIso8601String(),
|
'generatedAt': DateTime.now().toIso8601String(),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DioClient {
|
|||||||
receiveTimeout: timeout,
|
receiveTimeout: timeout,
|
||||||
sendTimeout: timeout,
|
sendTimeout: timeout,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'GelRuleApp/0.1 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
'Accept': 'application/json, text/xml;q=0.9, */*;q=0.8',
|
'Accept': 'application/json, text/xml;q=0.9, */*;q=0.8',
|
||||||
...?headers,
|
...?headers,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -188,6 +188,13 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
key: ValueKey(post.cacheKey),
|
key: ValueKey(post.cacheKey),
|
||||||
post: post,
|
post: post,
|
||||||
qualityMode: qualityMode,
|
qualityMode: qualityMode,
|
||||||
|
initialLoop: settings.videoPlayerLoop,
|
||||||
|
initialMuted: settings.videoPlayerMuted,
|
||||||
|
initialCoverVideo: settings.videoPlayerCover,
|
||||||
|
initialHalfVolume:
|
||||||
|
settings.videoPlayerHalfVolume,
|
||||||
|
onPlaybackPreferencesChanged: (snapshot) =>
|
||||||
|
_saveVideoPreferences(ref, snapshot),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -290,6 +297,12 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
key: ValueKey(post.cacheKey),
|
key: ValueKey(post.cacheKey),
|
||||||
post: post,
|
post: post,
|
||||||
qualityMode: qualityMode,
|
qualityMode: qualityMode,
|
||||||
|
initialLoop: settings.videoPlayerLoop,
|
||||||
|
initialMuted: settings.videoPlayerMuted,
|
||||||
|
initialCoverVideo: settings.videoPlayerCover,
|
||||||
|
initialHalfVolume: settings.videoPlayerHalfVolume,
|
||||||
|
onPlaybackPreferencesChanged: (snapshot) =>
|
||||||
|
_saveVideoPreferences(ref, snapshot),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -479,6 +492,30 @@ class PostDetailsScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
context.go('/');
|
context.go('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveVideoPreferences(
|
||||||
|
WidgetRef ref,
|
||||||
|
VideoPlaybackSnapshot snapshot,
|
||||||
|
) async {
|
||||||
|
final result = await ref.read(settingsServiceProvider).getSettings();
|
||||||
|
if (result is! Success<AppSettings>) return;
|
||||||
|
final settings = result.data;
|
||||||
|
if (settings.videoPlayerMuted == snapshot.muted &&
|
||||||
|
settings.videoPlayerHalfVolume == snapshot.halfVolume &&
|
||||||
|
settings.videoPlayerLoop == snapshot.loopVideo &&
|
||||||
|
settings.videoPlayerCover == snapshot.coverVideo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ref.read(settingsServiceProvider).updateSettings(
|
||||||
|
settings.copyWith(
|
||||||
|
videoPlayerMuted: snapshot.muted,
|
||||||
|
videoPlayerHalfVolume: snapshot.halfVolume,
|
||||||
|
videoPlayerLoop: snapshot.loopVideo,
|
||||||
|
videoPlayerCover: snapshot.coverVideo,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
ref.invalidate(appSettingsProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MobilePostPager extends StatefulWidget {
|
class _MobilePostPager extends StatefulWidget {
|
||||||
@@ -573,7 +610,7 @@ class _MobilePostPagerState extends State<_MobilePostPager> {
|
|||||||
|
|
||||||
Map<String, String> _headersFor(Post post) {
|
Map<String, String> _headersFor(Post post) {
|
||||||
return {
|
return {
|
||||||
'User-Agent': 'RuleGel/0.2 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
if (post.providerName.toLowerCase().contains('gelbooru') ||
|
if (post.providerName.toLowerCase().contains('gelbooru') ||
|
||||||
post.fileUrl.contains('gelbooru.com') ||
|
post.fileUrl.contains('gelbooru.com') ||
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PostMediaViewer extends StatefulWidget {
|
|||||||
this.initialHalfVolume = false,
|
this.initialHalfVolume = false,
|
||||||
this.qualityMode = MediaQualityMode.auto,
|
this.qualityMode = MediaQualityMode.auto,
|
||||||
this.onPlaybackSnapshot,
|
this.onPlaybackSnapshot,
|
||||||
|
this.onPlaybackPreferencesChanged,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ class PostMediaViewer extends StatefulWidget {
|
|||||||
final bool initialHalfVolume;
|
final bool initialHalfVolume;
|
||||||
final MediaQualityMode qualityMode;
|
final MediaQualityMode qualityMode;
|
||||||
final ValueChanged<VideoPlaybackSnapshot>? onPlaybackSnapshot;
|
final ValueChanged<VideoPlaybackSnapshot>? onPlaybackSnapshot;
|
||||||
|
final ValueChanged<VideoPlaybackSnapshot>? onPlaybackPreferencesChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostMediaViewer> createState() => _PostMediaViewerState();
|
State<PostMediaViewer> createState() => _PostMediaViewerState();
|
||||||
@@ -161,12 +163,14 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
onRetry: _retryVideo,
|
onRetry: _retryVideo,
|
||||||
onToggleFit: () {
|
onToggleFit: () {
|
||||||
setState(() => _coverVideo = !_coverVideo);
|
setState(() => _coverVideo = !_coverVideo);
|
||||||
|
_emitPlaybackPreferences();
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleMute: () async {
|
onToggleMute: () async {
|
||||||
final nextMuted = !_muted;
|
final nextMuted = !_muted;
|
||||||
setState(() => _muted = nextMuted);
|
setState(() => _muted = nextMuted);
|
||||||
await _applyVolume();
|
await _applyVolume();
|
||||||
|
_emitPlaybackPreferences();
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleHalfVolume: () async {
|
onToggleHalfVolume: () async {
|
||||||
@@ -175,6 +179,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
if (_halfVolume) _muted = false;
|
if (_halfVolume) _muted = false;
|
||||||
});
|
});
|
||||||
await _applyVolume();
|
await _applyVolume();
|
||||||
|
_emitPlaybackPreferences();
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleLoop: () async {
|
onToggleLoop: () async {
|
||||||
@@ -183,6 +188,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
await _player!.setPlaylistMode(
|
await _player!.setPlaylistMode(
|
||||||
nextLoop ? PlaylistMode.single : PlaylistMode.none,
|
nextLoop ? PlaylistMode.single : PlaylistMode.none,
|
||||||
);
|
);
|
||||||
|
_emitPlaybackPreferences();
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onFullscreen: widget.fullscreen
|
onFullscreen: widget.fullscreen
|
||||||
@@ -342,7 +348,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
|
|
||||||
Map<String, String> _headersFor(Post post) {
|
Map<String, String> _headersFor(Post post) {
|
||||||
return {
|
return {
|
||||||
'User-Agent': 'RuleGel/0.2 Flutter local booru browser',
|
'User-Agent': 'Lunaris/1.1 Flutter local booru browser',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
if (post.providerName.toLowerCase().contains('gelbooru') ||
|
if (post.providerName.toLowerCase().contains('gelbooru') ||
|
||||||
post.fileUrl.contains('gelbooru.com') ||
|
post.fileUrl.contains('gelbooru.com') ||
|
||||||
@@ -394,6 +400,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
coverVideo: _coverVideo,
|
coverVideo: _coverVideo,
|
||||||
errorMessage: _videoError,
|
errorMessage: _videoError,
|
||||||
onRetry: _retryVideo,
|
onRetry: _retryVideo,
|
||||||
|
onChanged: (_) => _emitPlaybackPreferences(),
|
||||||
),
|
),
|
||||||
transitionsBuilder: (_, animation, __, child) => FadeTransition(
|
transitionsBuilder: (_, animation, __, child) => FadeTransition(
|
||||||
opacity: animation,
|
opacity: animation,
|
||||||
@@ -414,6 +421,7 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
await player.setPlaylistMode(
|
await player.setPlaylistMode(
|
||||||
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
|
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
|
||||||
);
|
);
|
||||||
|
_emitPlaybackPreferences();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -438,6 +446,13 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
: 100);
|
: 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _emitPlaybackPreferences() {
|
||||||
|
final snapshot = _snapshot();
|
||||||
|
_playbackMemory[widget.post.cacheKey] = snapshot;
|
||||||
|
widget.onPlaybackSnapshot?.call(snapshot);
|
||||||
|
widget.onPlaybackPreferencesChanged?.call(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _togglePlay() async {
|
Future<void> _togglePlay() async {
|
||||||
final player = _player;
|
final player = _player;
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
@@ -750,6 +765,7 @@ class _FullscreenVideoPage extends StatefulWidget {
|
|||||||
required this.coverVideo,
|
required this.coverVideo,
|
||||||
required this.errorMessage,
|
required this.errorMessage,
|
||||||
required this.onRetry,
|
required this.onRetry,
|
||||||
|
required this.onChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Player player;
|
final Player player;
|
||||||
@@ -761,6 +777,7 @@ class _FullscreenVideoPage extends StatefulWidget {
|
|||||||
final bool coverVideo;
|
final bool coverVideo;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final VoidCallback onRetry;
|
final VoidCallback onRetry;
|
||||||
|
final ValueChanged<VideoPlaybackSnapshot> onChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FullscreenVideoPage> createState() => _FullscreenVideoPageState();
|
State<_FullscreenVideoPage> createState() => _FullscreenVideoPageState();
|
||||||
@@ -849,11 +866,13 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
|||||||
onRetry: widget.onRetry,
|
onRetry: widget.onRetry,
|
||||||
onToggleFit: () {
|
onToggleFit: () {
|
||||||
setState(() => _coverVideo = !_coverVideo);
|
setState(() => _coverVideo = !_coverVideo);
|
||||||
|
widget.onChanged(_snapshot());
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleMute: () async {
|
onToggleMute: () async {
|
||||||
setState(() => _muted = !_muted);
|
setState(() => _muted = !_muted);
|
||||||
await _applyVolume();
|
await _applyVolume();
|
||||||
|
widget.onChanged(_snapshot());
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleHalfVolume: () async {
|
onToggleHalfVolume: () async {
|
||||||
@@ -862,6 +881,7 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
|||||||
if (_halfVolume) _muted = false;
|
if (_halfVolume) _muted = false;
|
||||||
});
|
});
|
||||||
await _applyVolume();
|
await _applyVolume();
|
||||||
|
widget.onChanged(_snapshot());
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onToggleLoop: () async {
|
onToggleLoop: () async {
|
||||||
@@ -869,6 +889,7 @@ class _FullscreenVideoPageState extends State<_FullscreenVideoPage> {
|
|||||||
await widget.player.setPlaylistMode(
|
await widget.player.setPlaylistMode(
|
||||||
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
|
_loopVideo ? PlaylistMode.single : PlaylistMode.none,
|
||||||
);
|
);
|
||||||
|
widget.onChanged(_snapshot());
|
||||||
_showControls();
|
_showControls();
|
||||||
},
|
},
|
||||||
onFullscreen: () => Navigator.of(
|
onFullscreen: () => Navigator.of(
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ class _SettingsContent extends ConsumerWidget {
|
|||||||
final update = (result as Success<AppUpdateInfo?>).data;
|
final update = (result as Success<AppUpdateInfo?>).data;
|
||||||
if (update == null) {
|
if (update == null) {
|
||||||
messenger.showSnackBar(
|
messenger.showSnackBar(
|
||||||
const SnackBar(content: Text('RuleGel is up to date')),
|
const SnackBar(content: Text('Lunaris is up to date')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -407,7 +407,7 @@ class _SettingsContent extends ConsumerWidget {
|
|||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: Text('RuleGel ${info.version} is available'),
|
title: Text('Lunaris ${info.version} is available'),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 520),
|
constraints: const BoxConstraints(maxWidth: 520),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -454,7 +454,7 @@ class _SettingsContent extends ConsumerWidget {
|
|||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('RuleGel changelog'),
|
title: const Text('Lunaris changelog'),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 560),
|
constraints: const BoxConstraints(maxWidth: 560),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@@ -462,7 +462,7 @@ class _SettingsContent extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
for (final change in ruleGelChangelog) ...[
|
for (final change in lunarisChangelog) ...[
|
||||||
Text(
|
Text(
|
||||||
'${change.version} - ${change.title}',
|
'${change.version} - ${change.title}',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ static void my_application_activate(GApplication* application) {
|
|||||||
if (use_header_bar) {
|
if (use_header_bar) {
|
||||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
gtk_header_bar_set_title(header_bar, "gel_rule_app");
|
gtk_header_bar_set_title(header_bar, "Lunaris");
|
||||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
} else {
|
} else {
|
||||||
gtk_window_set_title(window, "gel_rule_app");
|
gtk_window_set_title(window, "Lunaris");
|
||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.0.2+10
|
version: 1.1.0+11
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.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/models/post.dart';
|
||||||
import 'package:gel_rule_app/backend/repositories/collection_repository.dart';
|
import 'package:gel_rule_app/backend/repositories/collection_repository.dart';
|
||||||
import 'package:gel_rule_app/backend/repositories/favorite_repository.dart';
|
import 'package:gel_rule_app/backend/repositories/favorite_repository.dart';
|
||||||
@@ -73,11 +74,52 @@ void main() {
|
|||||||
'e621',
|
'e621',
|
||||||
'e926',
|
'e926',
|
||||||
'realbooru',
|
'realbooru',
|
||||||
|
'xbooru',
|
||||||
|
'cosbooru',
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
providers.singleWhere((provider) => provider.id == 'realbooru').enabled,
|
providers.singleWhere((provider) => provider.id == 'realbooru').enabled,
|
||||||
isFalse,
|
isFalse,
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
providers.singleWhere((provider) => provider.id == 'cosbooru').enabled,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
final xbooru = providers.singleWhere((provider) => provider.id == 'xbooru');
|
||||||
|
expect(xbooru.enabled, isTrue);
|
||||||
|
expect(xbooru.baseUrl, 'https://xbooru.com');
|
||||||
|
expect(xbooru.apiType, 'gelbooru');
|
||||||
|
final cosbooru =
|
||||||
|
providers.singleWhere((provider) => provider.id == 'cosbooru');
|
||||||
|
expect(cosbooru.baseUrl, 'https://cos.lycore.co');
|
||||||
|
expect(cosbooru.apiType, 'danbooru');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('provider repository migrates old CosBooru config', () async {
|
||||||
|
final repository = ProviderRepository(databaseService);
|
||||||
|
final now = DateTime(2026);
|
||||||
|
await repository.saveProvider(ContentProviderConfig(
|
||||||
|
id: 'cosbooru',
|
||||||
|
name: 'CosBooru',
|
||||||
|
baseUrl: 'https://cos.booru.nl',
|
||||||
|
apiType: 'gelbooru',
|
||||||
|
enabled: false,
|
||||||
|
priority: 99,
|
||||||
|
timeoutSeconds: 10,
|
||||||
|
customHeaders: const {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
));
|
||||||
|
|
||||||
|
await repository.ensureSeedProviders();
|
||||||
|
final providers = (await repository.getProviders() as Success).data;
|
||||||
|
final cosbooru =
|
||||||
|
providers.singleWhere((provider) => provider.id == 'cosbooru');
|
||||||
|
|
||||||
|
expect(cosbooru.baseUrl, 'https://cos.lycore.co');
|
||||||
|
expect(cosbooru.apiType, 'danbooru');
|
||||||
|
expect(cosbooru.enabled, isTrue);
|
||||||
|
expect(cosbooru.priority, 9);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('settings saveEnabledProviders updates provider configs', () async {
|
test('settings saveEnabledProviders updates provider configs', () async {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:gel_rule_app/backend/mappers/gelbooru_mapper.dart';
|
|||||||
import 'package:gel_rule_app/backend/mappers/moebooru_mapper.dart';
|
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/mappers/rule34_mapper.dart';
|
||||||
import 'package:gel_rule_app/backend/models/content_provider_config.dart';
|
import 'package:gel_rule_app/backend/models/content_provider_config.dart';
|
||||||
|
import 'package:gel_rule_app/backend/providers/danbooru_provider.dart';
|
||||||
import 'package:gel_rule_app/backend/providers/provider_factory.dart';
|
import 'package:gel_rule_app/backend/providers/provider_factory.dart';
|
||||||
import 'package:gel_rule_app/backend/providers/realbooru_provider.dart';
|
import 'package:gel_rule_app/backend/providers/realbooru_provider.dart';
|
||||||
import 'package:gel_rule_app/backend/repositories/provider_repository.dart';
|
import 'package:gel_rule_app/backend/repositories/provider_repository.dart';
|
||||||
@@ -205,7 +206,7 @@ void main() {
|
|||||||
expect(posts.single.rating, 'unknown');
|
expect(posts.single.rating, 'unknown');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('seed providers include enabled Realbooru', () {
|
test('seed providers include CosBooru and no Paheal', () {
|
||||||
final realbooru = ProviderRepository.seedProviders()
|
final realbooru = ProviderRepository.seedProviders()
|
||||||
.where((provider) => provider.id == 'realbooru')
|
.where((provider) => provider.id == 'realbooru')
|
||||||
.single;
|
.single;
|
||||||
@@ -213,6 +214,23 @@ void main() {
|
|||||||
expect(realbooru.enabled, isFalse);
|
expect(realbooru.enabled, isFalse);
|
||||||
expect(realbooru.apiType, 'realbooru');
|
expect(realbooru.apiType, 'realbooru');
|
||||||
expect(realbooru.baseUrl, 'https://realbooru.com');
|
expect(realbooru.baseUrl, 'https://realbooru.com');
|
||||||
|
expect(
|
||||||
|
ProviderRepository.seedProviders()
|
||||||
|
.where((provider) => provider.id == 'paheal'),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
final cosbooru = ProviderRepository.seedProviders()
|
||||||
|
.where((provider) => provider.id == 'cosbooru')
|
||||||
|
.single;
|
||||||
|
final xbooru = ProviderRepository.seedProviders()
|
||||||
|
.where((provider) => provider.id == 'xbooru')
|
||||||
|
.single;
|
||||||
|
expect(xbooru.enabled, isTrue);
|
||||||
|
expect(xbooru.apiType, 'gelbooru');
|
||||||
|
expect(xbooru.baseUrl, 'https://xbooru.com');
|
||||||
|
expect(cosbooru.enabled, isTrue);
|
||||||
|
expect(cosbooru.apiType, 'danbooru');
|
||||||
|
expect(cosbooru.baseUrl, 'https://cos.lycore.co');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('provider factory creates Realbooru provider', () {
|
test('provider factory creates Realbooru provider', () {
|
||||||
@@ -232,4 +250,22 @@ void main() {
|
|||||||
|
|
||||||
expect(provider, isA<RealbooruProvider>());
|
expect(provider, isA<RealbooruProvider>());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('provider factory creates CosBooru as Danbooru provider', () {
|
||||||
|
final now = DateTime(2026);
|
||||||
|
final provider = ProviderFactory().create(ContentProviderConfig(
|
||||||
|
id: 'cosbooru',
|
||||||
|
name: 'CosBooru',
|
||||||
|
baseUrl: 'https://cos.lycore.co',
|
||||||
|
apiType: 'danbooru',
|
||||||
|
enabled: true,
|
||||||
|
priority: 8,
|
||||||
|
timeoutSeconds: 20,
|
||||||
|
customHeaders: const {},
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(provider, isA<DanbooruProvider>());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,12 +90,12 @@ BEGIN
|
|||||||
BLOCK "040904e4"
|
BLOCK "040904e4"
|
||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", "com.example" "\0"
|
VALUE "CompanyName", "com.example" "\0"
|
||||||
VALUE "FileDescription", "RuleGel" "\0"
|
VALUE "FileDescription", "Lunaris" "\0"
|
||||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||||
VALUE "InternalName", "RuleGel" "\0"
|
VALUE "InternalName", "Lunaris" "\0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||||
VALUE "OriginalFilename", "gel_rule_app.exe" "\0"
|
VALUE "OriginalFilename", "gel_rule_app.exe" "\0"
|
||||||
VALUE "ProductName", "RuleGel" "\0"
|
VALUE "ProductName", "Lunaris" "\0"
|
||||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(1280, 720);
|
Win32Window::Size size(1280, 720);
|
||||||
if (!window.Create(L"gel_rule_app", origin, size)) {
|
if (!window.Create(L"Lunaris", origin, size)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user