Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b33248ef9d |
@@ -10,7 +10,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Version-2.0.14-blueviolet?style=for-the-badge" alt="Version 2.0.14" />
|
<img src="https://img.shields.io/badge/Version-2.0.15-blueviolet?style=for-the-badge" alt="Version 2.0.15" />
|
||||||
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
<img src="https://img.shields.io/badge/Flutter-3.47+-02569B?style=for-the-badge&logo=flutter&logoColor=white" alt="Flutter" />
|
||||||
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" />
|
<img src="https://img.shields.io/badge/Platform-Android%20%7C%20Linux%20%7C%20Windows-brightgreen?style=for-the-badge" alt="Platforms" />
|
||||||
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
|
<img src="https://img.shields.io/badge/Gitea-RarDog%2FLunaris-orange?style=for-the-badge&logo=gitea&logoColor=white" alt="Gitea" />
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
const appDisplayVersion = '2.0.14';
|
const appDisplayVersion = '2.0.15';
|
||||||
const appBuildNumber = 36;
|
const appBuildNumber = 37;
|
||||||
const appReleaseChannel = 'Stable';
|
const appReleaseChannel = 'Stable';
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
const lunarisChangelog = [
|
const lunarisChangelog = [
|
||||||
|
LunarisChange(
|
||||||
|
version: '2.0.15',
|
||||||
|
title: 'Settings 2.0 Layout Fixes & Pinned Quick Navigation',
|
||||||
|
bullets: [
|
||||||
|
'Fixed vertical text wrapping on mobile: separated title and full-width SegmentedButtons for Theme, Language, and Media Quality so text never collapses.',
|
||||||
|
'Pinned Category Quick Navigation: bar now stays at the top of the screen and reliably scrolls to all 7 sections.',
|
||||||
|
'SingleChildScrollView Migration: ensures all section keys remain mounted and scrollable on demand.',
|
||||||
|
'Full-width Folder Structure Tile: added clean monospace path card with dedicated change button.',
|
||||||
|
'Header text overflow protections: wrapped title texts in Expanded across all settings cards.',
|
||||||
|
],
|
||||||
|
),
|
||||||
LunarisChange(
|
LunarisChange(
|
||||||
version: '2.0.14',
|
version: '2.0.14',
|
||||||
title: 'Settings 2.0: Modern Grouped Design & Quick Ergonomics',
|
title: 'Settings 2.0: Modern Grouped Design & Quick Ergonomics',
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
Scrollable.ensureVisible(
|
Scrollable.ensureVisible(
|
||||||
ctx,
|
ctx,
|
||||||
duration: const Duration(milliseconds: 380),
|
duration: const Duration(milliseconds: 380),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeInOutCubic,
|
||||||
alignment: 0.04,
|
alignment: 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +79,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
final isAndroid = Platform.isAndroid;
|
final isAndroid = Platform.isAndroid;
|
||||||
final strings = AppStrings(settings.languageCode);
|
final strings = AppStrings(settings.languageCode);
|
||||||
final isRu = strings.ru;
|
final isRu = strings.ru;
|
||||||
@@ -139,21 +140,43 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return Align(
|
return Column(
|
||||||
alignment: Alignment.topCenter,
|
children: [
|
||||||
|
// Pinned Top Category Quick Navigation
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 860),
|
constraints: const BoxConstraints(maxWidth: 860),
|
||||||
child: ListView(
|
child: _CategoryQuickNav(
|
||||||
controller: _scrollController,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
children: [
|
|
||||||
// Top Quick-Nav Bar
|
|
||||||
_CategoryQuickNav(
|
|
||||||
categories: navCategories,
|
categories: navCategories,
|
||||||
onSelect: _scrollTo,
|
onSelect: _scrollTo,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Scrollable Settings Cards
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 860),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
// 1. General Section
|
// 1. General Section
|
||||||
_SettingsCardGroup(
|
_SettingsCardGroup(
|
||||||
sectionKey: _generalKey,
|
sectionKey: _generalKey,
|
||||||
@@ -161,19 +184,13 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
icon: Icons.tune_rounded,
|
icon: Icons.tune_rounded,
|
||||||
accentColor: const Color(0xFF6366F1),
|
accentColor: const Color(0xFF6366F1),
|
||||||
children: [
|
children: [
|
||||||
_SettingsTile(
|
_SettingsSegmentedTile<String>(
|
||||||
icon: Icons.brightness_6_rounded,
|
icon: Icons.brightness_6_rounded,
|
||||||
iconColor: const Color(0xFF6366F1),
|
iconColor: const Color(0xFF6366F1),
|
||||||
title: strings.theme,
|
title: strings.theme,
|
||||||
subtitle: isRu
|
subtitle: isRu
|
||||||
? 'Оформление цветовой схемы приложения'
|
? 'Оформление цветовой схемы приложения'
|
||||||
: 'App theme brightness mode',
|
: 'App theme brightness mode',
|
||||||
trailing: SegmentedButton<String>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
segments: [
|
segments: [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: 'dark',
|
value: 'dark',
|
||||||
@@ -197,7 +214,6 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
settings.copyWith(themeMode: set.first),
|
settings.copyWith(themeMode: set.first),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const _SettingsDivider(),
|
const _SettingsDivider(),
|
||||||
_SettingsSwitchTile(
|
_SettingsSwitchTile(
|
||||||
icon: Icons.explicit_rounded,
|
icon: Icons.explicit_rounded,
|
||||||
@@ -281,27 +297,21 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
icon: Icons.palette_rounded,
|
icon: Icons.palette_rounded,
|
||||||
accentColor: const Color(0xFF8B5CF6),
|
accentColor: const Color(0xFF8B5CF6),
|
||||||
children: [
|
children: [
|
||||||
_SettingsTile(
|
_SettingsSegmentedTile<String>(
|
||||||
icon: Icons.translate_rounded,
|
icon: Icons.translate_rounded,
|
||||||
iconColor: const Color(0xFF8B5CF6),
|
iconColor: const Color(0xFF8B5CF6),
|
||||||
title: strings.language,
|
title: strings.language,
|
||||||
subtitle: isRu
|
subtitle: isRu
|
||||||
? 'Язык интерфейса приложения'
|
? 'Язык интерфейса приложения'
|
||||||
: 'App user interface language',
|
: 'App user interface language',
|
||||||
trailing: SegmentedButton<String>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
segments: const [
|
segments: const [
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: 'ru',
|
value: 'ru',
|
||||||
label: Text('🇷🇺 RU'),
|
label: Text('🇷🇺 Русский'),
|
||||||
),
|
),
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
value: 'en',
|
value: 'en',
|
||||||
label: Text('🇬🇧 EN'),
|
label: Text('🇬🇧 English'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
selected: {settings.languageCode},
|
selected: {settings.languageCode},
|
||||||
@@ -310,7 +320,6 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
settings.copyWith(languageCode: set.first),
|
settings.copyWith(languageCode: set.first),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const _SettingsDivider(),
|
const _SettingsDivider(),
|
||||||
_SettingsSwitchTile(
|
_SettingsSwitchTile(
|
||||||
icon: Icons.contrast_rounded,
|
icon: Icons.contrast_rounded,
|
||||||
@@ -389,19 +398,13 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
icon: Icons.dashboard_customize_rounded,
|
icon: Icons.dashboard_customize_rounded,
|
||||||
accentColor: const Color(0xFF10B981),
|
accentColor: const Color(0xFF10B981),
|
||||||
children: [
|
children: [
|
||||||
_SettingsTile(
|
_SettingsSegmentedTile<String>(
|
||||||
icon: Icons.high_quality_rounded,
|
icon: Icons.high_quality_rounded,
|
||||||
iconColor: const Color(0xFF10B981),
|
iconColor: const Color(0xFF10B981),
|
||||||
title: strings.mediaQuality,
|
title: strings.mediaQuality,
|
||||||
subtitle: isRu
|
subtitle: isRu
|
||||||
? 'Разрешение загружаемых картинок в сетке'
|
? 'Разрешение загружаемых картинок в сетке'
|
||||||
: 'Resolution profile for feed thumbnails',
|
: 'Resolution profile for feed thumbnails',
|
||||||
trailing: SegmentedButton<String>(
|
|
||||||
showSelectedIcon: false,
|
|
||||||
style: const ButtonStyle(
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
segments: [
|
segments: [
|
||||||
for (final mode in MediaQualityMode.values)
|
for (final mode in MediaQualityMode.values)
|
||||||
ButtonSegment(
|
ButtonSegment(
|
||||||
@@ -410,7 +413,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
isRu
|
isRu
|
||||||
? switch (mode) {
|
? switch (mode) {
|
||||||
MediaQualityMode.auto => 'Авто',
|
MediaQualityMode.auto => 'Авто',
|
||||||
MediaQualityMode.dataSaver => 'Трафик',
|
MediaQualityMode.dataSaver => 'Экономия',
|
||||||
MediaQualityMode.highQuality => 'HQ',
|
MediaQualityMode.highQuality => 'HQ',
|
||||||
}
|
}
|
||||||
: mode.label,
|
: mode.label,
|
||||||
@@ -423,7 +426,6 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
settings.copyWith(mediaQualityMode: set.first),
|
settings.copyWith(mediaQualityMode: set.first),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (isAndroid) ...[
|
if (isAndroid) ...[
|
||||||
const _SettingsDivider(),
|
const _SettingsDivider(),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -459,21 +461,16 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
isRu
|
isRu
|
||||||
? 'Частота обновления экрана'
|
? 'Частота обновления экрана'
|
||||||
: 'Animation refresh profile',
|
: 'Animation refresh profile',
|
||||||
style: Theme.of(context)
|
style: theme.textTheme.bodyLarge
|
||||||
.textTheme
|
|
||||||
.bodyLarge
|
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
?.copyWith(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
isRu
|
isRu
|
||||||
? 'Оптимизация плавности скролла ленты'
|
? 'Оптимизация плавности скролла ленты'
|
||||||
: 'Fluid 120-165 Hz scrolling optimization',
|
: 'Fluid 120-165 Hz scrolling optimization',
|
||||||
style: Theme.of(context)
|
style: theme.textTheme.bodyMedium
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.copyWith(
|
?.copyWith(
|
||||||
color: Theme.of(context)
|
color: theme.colorScheme
|
||||||
.colorScheme
|
|
||||||
.onSurfaceVariant,
|
.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -482,7 +479,7 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 12),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
@@ -534,8 +531,8 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
iconColor: const Color(0xFF3B82F6),
|
iconColor: const Color(0xFF3B82F6),
|
||||||
title: strings.desktopColumns,
|
title: strings.desktopColumns,
|
||||||
subtitle: isRu
|
subtitle: isRu
|
||||||
? 'Количество столбцов постов на ПК и широких экранах'
|
? 'Количество столбцов постов на ПК'
|
||||||
: 'Number of columns on desktop layout',
|
: 'Columns on desktop layout',
|
||||||
value: settings.desktopColumns,
|
value: settings.desktopColumns,
|
||||||
min: 3,
|
min: 3,
|
||||||
max: 8,
|
max: 8,
|
||||||
@@ -548,8 +545,8 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
iconColor: const Color(0xFF6366F1),
|
iconColor: const Color(0xFF6366F1),
|
||||||
title: strings.mobileColumns,
|
title: strings.mobileColumns,
|
||||||
subtitle: isRu
|
subtitle: isRu
|
||||||
? 'Количество столбцов постов на телефонах'
|
? 'Количество столбцов постов на телефоне'
|
||||||
: 'Number of columns on phone layout',
|
: 'Columns on phone layout',
|
||||||
value: settings.mobileColumns,
|
value: settings.mobileColumns,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 3,
|
max: 3,
|
||||||
@@ -816,6 +813,10 @@ class _SettingsContentState extends ConsumerState<_SettingsContent> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,8 +1059,9 @@ class _CategoryQuickNav extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 44,
|
height: 40,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: categories.length,
|
itemCount: categories.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
separatorBuilder: (_, __) => const SizedBox(width: 8),
|
||||||
@@ -1067,14 +1069,14 @@ class _CategoryQuickNav extends StatelessWidget {
|
|||||||
final cat = categories[index];
|
final cat = categories[index];
|
||||||
return Material(
|
return Material(
|
||||||
color: theme.colorScheme.surfaceContainer,
|
color: theme.colorScheme.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(20),
|
||||||
onTap: () => onSelect(cat.key),
|
onTap: () => onSelect(cat.key),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(22),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||||
),
|
),
|
||||||
@@ -1148,13 +1150,15 @@ class _SettingsCardGroup extends StatelessWidget {
|
|||||||
child: Icon(icon, size: 18, color: accentColor),
|
child: Icon(icon, size: 18, color: accentColor),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1250,6 +1254,90 @@ class _SettingsTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full-width segmented tile so titles never get squeezed vertically on narrow screens.
|
||||||
|
class _SettingsSegmentedTile<T> extends StatelessWidget {
|
||||||
|
const _SettingsSegmentedTile({
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.title,
|
||||||
|
required this.segments,
|
||||||
|
required this.selected,
|
||||||
|
required this.onSelectionChanged,
|
||||||
|
this.subtitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final List<ButtonSegment<T>> segments;
|
||||||
|
final Set<T> selected;
|
||||||
|
final ValueChanged<Set<T>> onSelectionChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: iconColor.withValues(alpha: 0.12),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 20, color: iconColor),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
subtitle!,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: SegmentedButton<T>(
|
||||||
|
showSelectedIcon: false,
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
segments: segments,
|
||||||
|
selected: selected,
|
||||||
|
onSelectionChanged: onSelectionChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _SettingsSwitchTile extends StatelessWidget {
|
class _SettingsSwitchTile extends StatelessWidget {
|
||||||
const _SettingsSwitchTile({
|
const _SettingsSwitchTile({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
@@ -1404,44 +1492,96 @@ class _SettingsFolderStructureTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return _SettingsTile(
|
return InkWell(
|
||||||
icon: Icons.folder_copy_rounded,
|
onTap: () => _showPicker(context),
|
||||||
iconColor: const Color(0xFF0EA5E9),
|
child: Padding(
|
||||||
title: isRu ? 'Папки скачивания' : 'Download folder structure',
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
subtitle: isRu
|
child: Column(
|
||||||
? 'Шаблон организации сохраняемых файлов'
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
: 'Path template for organized downloads',
|
children: [
|
||||||
trailing: Container(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainerHighest,
|
color: const Color(0xFF0EA5E9).withValues(alpha: 0.12),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
),
|
||||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.4),
|
child: const Icon(
|
||||||
|
Icons.folder_copy_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: Color(0xFF0EA5E9),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
const SizedBox(width: 14),
|
||||||
mainAxisSize: MainAxisSize.min,
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
currentTemplate,
|
isRu ? 'Папки скачивания' : 'Download folder structure',
|
||||||
style: TextStyle(
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(height: 2),
|
||||||
Icon(
|
Text(
|
||||||
Icons.unfold_more_rounded,
|
isRu
|
||||||
size: 16,
|
? 'Шаблон организации сохраняемых файлов'
|
||||||
|
: 'Path template for organized downloads',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
Icons.chevron_right_rounded,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.35),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.folder_open_rounded, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
currentTemplate,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
isRu ? 'Выбрать' : 'Select',
|
||||||
|
style: theme.textTheme.labelMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => _showPicker(context),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1627,12 +1767,14 @@ class _TagListEditorState extends State<_TagListEditor> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(widget.icon, size: 20, color: widget.accentColor),
|
Icon(widget.icon, size: 20, color: widget.accentColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
@@ -1743,12 +1885,14 @@ class _ColorSwatches extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.palette_outlined, size: 20, color: Color(0xFF8B5CF6)),
|
const Icon(Icons.palette_outlined, size: 20, color: Color(0xFF8B5CF6)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
'Акцентный цвет интерфейса',
|
'Акцентный цвет интерфейса',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -1830,12 +1974,14 @@ class _TabVisibilityEditor extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.tab_rounded, size: 20, color: Color(0xFF8B5CF6)),
|
const Icon(Icons.tab_rounded, size: 20, color: Color(0xFF8B5CF6)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Expanded(
|
||||||
|
child: Text(
|
||||||
'Отображение вкладок навигации',
|
'Отображение вкладок навигации',
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|||||||
+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: 2.0.14+36
|
version: 2.0.15+37
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import 'package:gel_rule_app/features/settings/presentation/settings_controller.
|
|||||||
import 'package:gel_rule_app/features/settings/presentation/settings_screen.dart';
|
import 'package:gel_rule_app/features/settings/presentation/settings_screen.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('SettingsScreen renders category quick nav and sections', (tester) async {
|
testWidgets('SettingsScreen renders category quick nav and sections without overflow', (tester) async {
|
||||||
tester.view.physicalSize = const Size(1200, 2400);
|
// Test on typical mobile viewport: 390x844
|
||||||
|
tester.view.physicalSize = const Size(390, 844);
|
||||||
tester.view.devicePixelRatio = 1.0;
|
tester.view.devicePixelRatio = 1.0;
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
tester.view.resetPhysicalSize();
|
tester.view.resetPhysicalSize();
|
||||||
@@ -34,22 +35,25 @@ void main() {
|
|||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Verify Category Quick Nav renders
|
// Verify pinned Category Quick Nav renders
|
||||||
expect(find.text('Основное'), findsWidgets);
|
expect(find.text('Основное'), findsWidgets);
|
||||||
expect(find.text('Внешний вид'), findsWidgets);
|
expect(find.text('Внешний вид'), findsWidgets);
|
||||||
expect(find.text('Лента и раскладка'), findsWidgets);
|
|
||||||
expect(find.text('Фильтры'), findsWidgets);
|
|
||||||
expect(find.text('Хранилище'), findsWidgets);
|
|
||||||
|
|
||||||
// Verify Theme mode segment buttons
|
// Verify Theme mode segment buttons
|
||||||
expect(find.text('Темная'), findsOneWidget);
|
expect(find.text('Темная'), findsOneWidget);
|
||||||
expect(find.text('Светлая'), findsOneWidget);
|
expect(find.text('Светлая'), findsOneWidget);
|
||||||
// Both ThemeMode and MediaQualityMode have 'Авто'
|
|
||||||
expect(find.text('Авто'), findsNWidgets(2));
|
|
||||||
|
|
||||||
// Verify Language segmented buttons
|
// Tap a quick nav chip to test scrolling
|
||||||
expect(find.text('🇷🇺 RU'), findsOneWidget);
|
await tester.tap(find.text('Внешний вид').first);
|
||||||
expect(find.text('🇬🇧 EN'), findsOneWidget);
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Verify language segment is visible and tappable
|
||||||
|
expect(find.text('🇷🇺 Русский'), findsOneWidget);
|
||||||
|
expect(find.text('🇬🇧 English'), findsOneWidget);
|
||||||
|
|
||||||
|
// Tap English to test selection
|
||||||
|
await tester.tap(find.text('🇬🇧 English'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user