Compare commits

...
5 Commits
Author SHA1 Message Date
RarDog bae64cd8c2 feat: widen tag suggestions dropdown and allow 2-line wrap (v3.0.4)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 17:18:50 +03:00
RarDog 5586fea6b3 feat: recommend GitHub for speed in update sheet, Gitea as neutral (v3.0.3)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 17:04:46 +03:00
RarDog d0419fdf80 docs: update version badge to 3.0.2 in README 2026-09-04 17:00:50 +03:00
RarDog f60c767a0d feat: add update source picker (Gitea recommended vs GitHub) (v3.0.2)
Build and Release / Build Windows x64 (push) Has been cancelled
2026-09-04 16:56:19 +03:00
RarDog 40ac7cd055 chore: sync repository metadata 2026-09-04 16:45:41 +03:00
6 changed files with 291 additions and 36 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
</p>
<p align="center">
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.0.1-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.0.1" /></a>
<a href="https://github.com/RarDog/Lunaris/releases"><img src="https://img.shields.io/badge/Version-3.0.4-8A2BE2?style=for-the-badge&logo=semver&logoColor=white" alt="Version 3.0.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" />
<a href="https://github.com/RarDog/Lunaris/wiki"><img src="https://img.shields.io/badge/Wiki-Documentation-FF6F00?style=for-the-badge&logo=gitbook&logoColor=white" alt="Wiki" /></a>
+2 -2
View File
@@ -1,3 +1,3 @@
const appDisplayVersion = '3.0.1';
const appBuildNumber = 52;
const appDisplayVersion = '3.0.4';
const appBuildNumber = 55;
const appReleaseChannel = 'Stable';
+44 -20
View File
@@ -9,14 +9,27 @@ import '../../core/utils/result.dart';
import '../models/app_update_info.dart';
import 'settings_service.dart';
enum UpdateSource {
gitea,
github,
}
class UpdateService {
UpdateService(this._dio, this._settingsService);
static const latestReleaseUrl =
static const giteaLatestReleaseUrl =
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases/latest';
static const giteaReleasesUrl =
'https://gitea.rardogsynapse.online/api/v1/repos/RarDog/Lunaris/releases';
static const githubLatestReleaseUrl =
'https://api.github.com/repos/RarDog/Lunaris/releases/latest';
static const releasesUrl =
static const githubReleasesUrl =
'https://api.github.com/repos/RarDog/Lunaris/releases';
static const latestReleaseUrl = githubLatestReleaseUrl;
static const releasesUrl = githubReleasesUrl;
static const _deviceChannel = MethodChannel('rulegel/device');
final Dio _dio;
@@ -50,14 +63,17 @@ class UpdateService {
}
}
Future<Result<AppUpdateInfo?>> checkForUpdates({bool force = false}) async {
Future<Result<AppUpdateInfo?>> checkForUpdates({
bool force = false,
UpdateSource source = UpdateSource.gitea,
}) async {
final settingsResult = await _settingsService.getSettings();
final settings = settingsResult is Success<AppSettings>
? settingsResult.data
: AppSettings.defaults;
try {
final info = await _latestAllowedRelease(settings);
final info = await _latestAllowedRelease(settings, source: source);
final nextSettings = settings.copyWith(
lastUpdateCheckAt: DateTime.now().toIso8601String(),
);
@@ -81,16 +97,29 @@ class UpdateService {
}
}
Future<AppUpdateInfo> _latestAllowedRelease(AppSettings settings) async {
Future<AppUpdateInfo> _latestAllowedRelease(
AppSettings settings, {
UpdateSource source = UpdateSource.gitea,
}) async {
final isGitea = source == UpdateSource.gitea;
final rUrl = isGitea ? giteaReleasesUrl : githubReleasesUrl;
final latestUrl = isGitea ? giteaLatestReleaseUrl : githubLatestReleaseUrl;
final options = Options(
headers: isGitea
? {
'Authorization': 'token 1c744d6044d756759d7b1f693c94c80cf70d75fa',
}
: {
'User-Agent': 'Lunaris-App',
'Accept': 'application/vnd.github+json',
},
);
final response = await _dio.get<dynamic>(
releasesUrl,
queryParameters: {'per_page': 30},
options: Options(
headers: {
'User-Agent': 'Lunaris-App',
'Accept': 'application/vnd.github+json',
},
),
rUrl,
queryParameters: isGitea ? {'limit': 30} : {'per_page': 30},
options: options,
);
final items = (response.data as List?) ?? const [];
final releases = items
@@ -102,13 +131,8 @@ class UpdateService {
.toList(growable: false);
if (releases.isEmpty) {
final latest = await _dio.get<dynamic>(
latestReleaseUrl,
options: Options(
headers: {
'User-Agent': 'Lunaris-App',
'Accept': 'application/vnd.github+json',
},
),
latestUrl,
options: options,
);
return _releaseFromJson(Map<String, dynamic>.from(latest.data as Map));
}
@@ -958,12 +958,216 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
}
Future<void> _checkUpdates(BuildContext context, WidgetRef ref) async {
final isRu = settings.languageCode == 'ru';
final theme = Theme.of(context);
final selectedSource = await showModalBottomSheet<UpdateSource>(
context: context,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 28),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 36,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
),
Text(
isRu ? 'Источник обновлений' : 'Update Source',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
isRu
? 'Выберите сервер для загрузки обновлений'
: 'Choose a server to download updates from',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
// Option 1: GitHub (Recommended for speed)
Material(
color: theme.colorScheme.primaryContainer.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => Navigator.pop(sheetContext, UpdateSource.github),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.bolt_rounded,
color: theme.colorScheme.primary,
size: 24,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'GitHub',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(6),
),
child: Text(
isRu ? 'Рекомендуется для скорости' : 'Recommended for speed',
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
),
],
),
const SizedBox(height: 3),
Text(
isRu
? 'Прямая быстрая загрузка без ограничений'
: 'Direct fast download without limits',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
],
),
),
),
),
const SizedBox(height: 12),
// Option 2: Gitea (Muted / neutral grey)
Material(
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => Navigator.pop(sheetContext, UpdateSource.gitea),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: theme.colorScheme.onSurface.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.dns_rounded,
color: theme.colorScheme.onSurfaceVariant,
size: 22,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Gitea',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 3),
Text(
isRu
? 'Сервер релизов'
: 'Release server',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
),
Icon(
Icons.arrow_forward_ios_rounded,
size: 14,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
],
),
),
),
),
],
),
);
},
);
if (selectedSource == null) return;
if (!context.mounted) return;
final messenger = ScaffoldMessenger.of(context);
messenger.showSnackBar(
const SnackBar(content: Text('Checking for updates...')),
SnackBar(
content: Text(
selectedSource == UpdateSource.gitea
? (isRu ? 'Проверка обновлений на Gitea...' : 'Checking updates on Gitea...')
: (isRu ? 'Проверка обновлений на GitHub...' : 'Checking updates on GitHub...'),
),
),
);
final result =
await ref.read(updateServiceProvider).checkForUpdates(force: true);
final result = await ref.read(updateServiceProvider).checkForUpdates(
force: true,
source: selectedSource,
);
if (!context.mounted) return;
messenger.hideCurrentSnackBar();
if (result is Error<AppUpdateInfo?>) {
@@ -973,7 +1177,11 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
final update = (result as Success<AppUpdateInfo?>).data;
if (update == null) {
messenger.showSnackBar(
const SnackBar(content: Text('Lunaris is up to date')),
SnackBar(
content: Text(
isRu ? 'У вас установлена последняя версия Lunaris' : 'Lunaris is up to date',
),
),
);
return;
}
+31 -8
View File
@@ -192,23 +192,46 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
return const SizedBox.shrink();
}
final screenSize = MediaQuery.sizeOf(context);
final renderBox =
_fieldKey.currentContext?.findRenderObject() as RenderBox?;
final width =
final fieldWidth =
renderBox?.hasSize == true ? renderBox!.size.width : 360.0;
final height =
final fieldHeight =
renderBox?.hasSize == true ? renderBox!.size.height : 48.0;
final screenW = screenSize.width;
// Substantially wider dropdown:
// Mobile (< 600px): use full screen width minus comfortable 24px margins.
// Desktop/Tablet (>= 600px): expand to 1.35x field width, up to 780px.
final dropdownWidth = screenW < 600
? (screenW - 24.0).clamp(fieldWidth, 580.0)
: (fieldWidth * 1.35).clamp(fieldWidth, (screenW - 32.0).clamp(fieldWidth, 780.0));
// Ensure dropdown stays fully on screen and doesn't get clipped
double dxOffset = 0.0;
if (renderBox?.hasSize == true && renderBox?.attached == true) {
final globalPos = renderBox!.localToGlobal(Offset.zero);
final globalRight = globalPos.dx + dropdownWidth;
final maxRight = screenW - 12.0;
if (globalRight > maxRight) {
dxOffset = maxRight - globalRight;
}
if (globalPos.dx + dxOffset < 12.0) {
dxOffset = 12.0 - globalPos.dx;
}
}
return CompositedTransformFollower(
link: _layerLink,
showWhenUnlinked: false,
offset: Offset(0, height + 6),
offset: Offset(dxOffset, fieldHeight + 6),
child: Align(
alignment: Alignment.topLeft,
child: TapRegion(
groupId: _fieldKey,
child: SizedBox(
width: width.clamp(280.0, 620.0),
width: dropdownWidth,
child: _TagSuggestionDropdown(
suggestions: suggestions,
query: token,
@@ -565,7 +588,7 @@ class _TagSuggestionDropdown extends StatelessWidget {
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 620),
constraints: const BoxConstraints(maxWidth: 820),
child: Material(
elevation: 8,
shadowColor: Colors.black.withValues(alpha: 0.18),
@@ -641,7 +664,7 @@ class _TagSuggestionDropdown extends StatelessWidget {
if (cleanToken.isEmpty) {
return Text(
text,
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: defaultStyle,
);
@@ -653,7 +676,7 @@ class _TagSuggestionDropdown extends StatelessWidget {
if (matchIndex < 0) {
return Text(
text,
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: defaultStyle,
);
@@ -678,7 +701,7 @@ class _TagSuggestionDropdown extends StatelessWidget {
if (after.isNotEmpty) TextSpan(text: after),
],
),
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
+1 -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.0.1+52
version: 3.0.4+55
environment:
sdk: ">=3.3.0 <4.0.0"