improved settings rebuild
This commit is contained in:
parent
45153e94bb
commit
4345f46cc2
26 changed files with 771 additions and 540 deletions
|
@ -597,6 +597,23 @@
|
|||
"settingsVideoQuickActionEditorTitle": "Quick Video Actions",
|
||||
"@settingsVideoQuickActionEditorTitle": {},
|
||||
|
||||
"settingsSubtitleThemeTile": "Subtitles",
|
||||
"@settingsSubtitleThemeTile": {},
|
||||
"settingsSubtitleThemeTitle": "Subtitles",
|
||||
"@settingsSubtitleThemeTitle": {},
|
||||
"settingsSubtitleThemeSample": "This is a sample.",
|
||||
"@settingsSubtitleThemeSample": {},
|
||||
"settingsSubtitleThemeTextAlignmentTile": "Text alignment",
|
||||
"@settingsSubtitleThemeTextAlignmentTile": {},
|
||||
"settingsSubtitleThemeTextAlignmentTitle": "Text Alignment",
|
||||
"@settingsSubtitleThemeTextAlignmentTitle": {},
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "Left",
|
||||
"@settingsSubtitleThemeTextAlignmentLeft": {},
|
||||
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
||||
"@settingsSubtitleThemeTextAlignmentCenter": {},
|
||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||
"@settingsSubtitleThemeTextAlignmentRight": {},
|
||||
|
||||
"settingsSectionPrivacy": "Privacy",
|
||||
"@settingsSectionPrivacy": {},
|
||||
"settingsEnableAnalytics": "Allow anonymous analytics and crash reporting",
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:pedantic/pedantic.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
@ -57,6 +58,13 @@ class Settings extends ChangeNotifier {
|
|||
static const videoLoopModeKey = 'video_loop';
|
||||
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
||||
|
||||
// subtitles
|
||||
static const subtitleFontSizeKey = 'subtitle_font_size';
|
||||
static const subtitleTextAlignmentKey = 'subtitle_text_alignment';
|
||||
static const subtitleShowOutlineKey = 'subtitle_show_outline';
|
||||
static const subtitleTextColorKey = 'subtitle_text_color';
|
||||
static const subtitleBackgroundColorKey = 'subtitle_background_color';
|
||||
|
||||
// info
|
||||
static const infoMapStyleKey = 'info_map_style';
|
||||
static const infoMapZoomKey = 'info_map_zoom';
|
||||
|
@ -241,21 +249,43 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// video
|
||||
|
||||
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
||||
|
||||
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true);
|
||||
|
||||
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
|
||||
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
||||
|
||||
bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false);
|
||||
|
||||
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
|
||||
|
||||
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values);
|
||||
|
||||
set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString());
|
||||
|
||||
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
|
||||
|
||||
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
|
||||
|
||||
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
|
||||
// subtitles
|
||||
|
||||
double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? 20;
|
||||
|
||||
set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue);
|
||||
|
||||
TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, TextAlign.center, TextAlign.values);
|
||||
|
||||
set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString());
|
||||
|
||||
bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, true);
|
||||
|
||||
set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue);
|
||||
|
||||
Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? Colors.white.value);
|
||||
|
||||
set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value);
|
||||
|
||||
Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? Colors.transparent.value);
|
||||
|
||||
set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value);
|
||||
|
||||
// info
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ class Durations {
|
|||
// settings animations
|
||||
static const quickActionListAnimation = Duration(milliseconds: 200);
|
||||
static const quickActionHighlightAnimation = Duration(milliseconds: 200);
|
||||
static const themeChangeDuration = Duration(milliseconds: 400);
|
||||
|
||||
// delays & refresh intervals
|
||||
static const opToastDisplay = Duration(seconds: 3);
|
||||
|
|
|
@ -27,22 +27,23 @@ class OutlinedText extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
ImageFiltered(
|
||||
imageFilter: outlineBlurSigma > 0
|
||||
? ImageFilter.blur(
|
||||
sigmaX: outlineBlurSigma,
|
||||
sigmaY: outlineBlurSigma,
|
||||
)
|
||||
: ImageFilter.matrix(
|
||||
Matrix4.identity().storage,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: textSpans.map(_toStrokeSpan).toList(),
|
||||
if (outlineWidth > 0)
|
||||
ImageFiltered(
|
||||
imageFilter: outlineBlurSigma > 0
|
||||
? ImageFilter.blur(
|
||||
sigmaX: outlineBlurSigma,
|
||||
sigmaY: outlineBlurSigma,
|
||||
)
|
||||
: ImageFilter.matrix(
|
||||
Matrix4.identity().storage,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
children: textSpans.map(_toStrokeSpan).toList(),
|
||||
),
|
||||
textAlign: textAlign,
|
||||
),
|
||||
textAlign: textAlign,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: textSpans,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/placeholder.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class AvailableActionPanel<T extends Object> extends StatelessWidget {
|
|
@ -6,11 +6,11 @@ import 'package:aves/utils/change_notifier.dart';
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/action_panel.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/available_actions.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/placeholder.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/quick_actions.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/action_panel.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/available_actions.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
34
lib/widgets/settings/common/tile_leading.dart
Normal file
34
lib/widgets/settings/common/tile_leading.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsTileLeading extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const SettingsTileLeading({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: color,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
)),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: DecoratedIcon(
|
||||
icon,
|
||||
shadows: Constants.embossShadows,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
61
lib/widgets/settings/language/language.dart
Normal file
61
lib/widgets/settings/language/language.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'package:aves/model/settings/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/language/locale.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LanguageSection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const LanguageSection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentCoordinateFormat = context.select<Settings, CoordinateFormat>((s) => s.coordinateFormat);
|
||||
|
||||
return AvesExpansionTile(
|
||||
// use a fixed value instead of the title to identify this expansion tile
|
||||
// so that the tile state is kept when the language is modified
|
||||
value: 'language',
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.language,
|
||||
color: stringToColor('Language'),
|
||||
),
|
||||
title: context.l10n.settingsSectionLanguage,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
LocaleTile(),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsCoordinateFormatTile),
|
||||
subtitle: Text(currentCoordinateFormat.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<CoordinateFormat>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||
initialValue: currentCoordinateFormat,
|
||||
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
optionSubtitleBuilder: (value) => value.format(Constants.pointNemo),
|
||||
title: context.l10n.settingsCoordinateFormatTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.coordinateFormat = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,10 +7,9 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class LanguageTile extends StatelessWidget {
|
||||
class LocaleTile extends StatelessWidget {
|
||||
static const _systemLocaleOption = Locale('system');
|
||||
|
||||
@override
|
79
lib/widgets/settings/navigation.dart
Normal file
79
lib/widgets/settings/navigation.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/home_page.dart';
|
||||
import 'package:aves/model/settings/screen_on.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class NavigationSection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const NavigationSection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentHomePage = context.select<Settings, HomePageSetting>((s) => s.homePage);
|
||||
final currentKeepScreenOn = context.select<Settings, KeepScreenOn>((s) => s.keepScreenOn);
|
||||
final currentMustBackTwiceToExit = context.select<Settings, bool>((s) => s.mustBackTwiceToExit);
|
||||
|
||||
return AvesExpansionTile(
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.home,
|
||||
color: stringToColor('Navigation'),
|
||||
),
|
||||
title: context.l10n.settingsSectionNavigation,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsHome),
|
||||
subtitle: Text(currentHomePage.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<HomePageSetting>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||
initialValue: currentHomePage,
|
||||
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsHome,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.homePage = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsKeepScreenOnTile),
|
||||
subtitle: Text(currentKeepScreenOn.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<KeepScreenOn>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
||||
initialValue: currentKeepScreenOn,
|
||||
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsKeepScreenOnTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.keepScreenOn = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentMustBackTwiceToExit,
|
||||
onChanged: (v) => settings.mustBackTwiceToExit = v,
|
||||
title: Text(context.l10n.settingsDoubleBackExit),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -52,16 +53,16 @@ class HiddenFilterPage extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Consumer<Settings>(
|
||||
builder: (context, settings, child) {
|
||||
final hiddenFilters = settings.hiddenFilters;
|
||||
final filterList = hiddenFilters.toList()..sort();
|
||||
child: Selector<Settings, Set<CollectionFilter>>(
|
||||
selector: (context, s) => settings.hiddenFilters,
|
||||
builder: (context, hiddenFilters, child) {
|
||||
if (hiddenFilters.isEmpty) {
|
||||
return EmptyContent(
|
||||
icon: AIcons.hide,
|
||||
text: context.l10n.settingsHiddenFiltersEmpty,
|
||||
);
|
||||
}
|
||||
final filterList = hiddenFilters.toList()..sort();
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
54
lib/widgets/settings/privacy/privacy.dart
Normal file
54
lib/widgets/settings/privacy/privacy.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/privacy/access_grants.dart';
|
||||
import 'package:aves/widgets/settings/privacy/hidden_filters.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PrivacySection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const PrivacySection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentIsCrashlyticsEnabled = context.select<Settings, bool>((s) => s.isCrashlyticsEnabled);
|
||||
final currentSaveSearchHistory = context.select<Settings, bool>((s) => s.saveSearchHistory);
|
||||
|
||||
return AvesExpansionTile(
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.privacy,
|
||||
color: stringToColor('Privacy'),
|
||||
),
|
||||
title: context.l10n.settingsSectionPrivacy,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: currentIsCrashlyticsEnabled,
|
||||
onChanged: (v) => settings.isCrashlyticsEnabled = v,
|
||||
title: Text(context.l10n.settingsEnableAnalytics),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentSaveSearchHistory,
|
||||
onChanged: (v) {
|
||||
settings.saveSearchHistory = v;
|
||||
if (!v) {
|
||||
settings.searchHistory = [];
|
||||
}
|
||||
},
|
||||
title: Text(context.l10n.settingsSaveSearchHistory),
|
||||
),
|
||||
HiddenFilterTile(),
|
||||
StorageAccessTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,30 +1,14 @@
|
|||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/coordinate_format.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/home_page.dart';
|
||||
import 'package:aves/model/settings/screen_on.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/settings/video_loop_mode.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/access_grants.dart';
|
||||
import 'package:aves/widgets/settings/entry_background.dart';
|
||||
import 'package:aves/widgets/settings/hidden_filters.dart';
|
||||
import 'package:aves/widgets/settings/language.dart';
|
||||
import 'package:aves/widgets/settings/video_actions_editor.dart';
|
||||
import 'package:aves/widgets/settings/viewer_actions_editor.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:aves/widgets/settings/language/language.dart';
|
||||
import 'package:aves/widgets/settings/navigation.dart';
|
||||
import 'package:aves/widgets/settings/privacy/privacy.dart';
|
||||
import 'package:aves/widgets/settings/thumbnails.dart';
|
||||
import 'package:aves/widgets/settings/video/video.dart';
|
||||
import 'package:aves/widgets/settings/viewer/viewer.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
static const routeName = '/settings';
|
||||
|
@ -52,28 +36,26 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Consumer<Settings>(
|
||||
builder: (context, settings, child) => AnimationLimiter(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: Durations.staggeredAnimation,
|
||||
delay: Durations.staggeredAnimationDelay,
|
||||
childAnimationBuilder: (child) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: child,
|
||||
),
|
||||
child: AnimationLimiter(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: Durations.staggeredAnimation,
|
||||
delay: Durations.staggeredAnimationDelay,
|
||||
childAnimationBuilder: (child) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: child,
|
||||
),
|
||||
children: [
|
||||
_buildNavigationSection(context),
|
||||
_buildThumbnailsSection(context),
|
||||
_buildViewerSection(context),
|
||||
_buildVideoSection(context),
|
||||
_buildPrivacySection(context),
|
||||
_buildLanguageSection(context),
|
||||
],
|
||||
),
|
||||
children: [
|
||||
NavigationSection(expandedNotifier: _expandedNotifier),
|
||||
ThumbnailsSection(expandedNotifier: _expandedNotifier),
|
||||
ViewerSection(expandedNotifier: _expandedNotifier),
|
||||
VideoSection(expandedNotifier: _expandedNotifier),
|
||||
PrivacySection(expandedNotifier: _expandedNotifier),
|
||||
LanguageSection(expandedNotifier: _expandedNotifier),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -82,271 +64,4 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavigationSection(BuildContext context) {
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.home, stringToColor('Navigation')),
|
||||
title: context.l10n.settingsSectionNavigation,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsHome),
|
||||
subtitle: Text(settings.homePage.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<HomePageSetting>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||
initialValue: settings.homePage,
|
||||
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsHome,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.homePage = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsKeepScreenOnTile),
|
||||
subtitle: Text(settings.keepScreenOn.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<KeepScreenOn>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
||||
initialValue: settings.keepScreenOn,
|
||||
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsKeepScreenOnTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.keepScreenOn = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.mustBackTwiceToExit,
|
||||
onChanged: (v) => settings.mustBackTwiceToExit = v,
|
||||
title: Text(context.l10n.settingsDoubleBackExit),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThumbnailsSection(BuildContext context) {
|
||||
final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor;
|
||||
double opacityFor(bool enabled) => enabled ? 1 : .2;
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')),
|
||||
title: context.l10n.settingsSectionThumbnails,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: settings.showThumbnailLocation,
|
||||
onChanged: (v) => settings.showThumbnailLocation = v,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(settings.showThumbnailLocation),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.location,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.showThumbnailRaw,
|
||||
onChanged: (v) => settings.showThumbnailRaw = v,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(settings.showThumbnailRaw),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.raw,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.showThumbnailVideoDuration,
|
||||
onChanged: (v) => settings.showThumbnailVideoDuration = v,
|
||||
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewerSection(BuildContext context) {
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.image, stringToColor('Image')),
|
||||
title: context.l10n.settingsSectionViewer,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
ViewerActionsTile(),
|
||||
SwitchListTile(
|
||||
value: settings.showOverlayMinimap,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
title: Text(context.l10n.settingsViewerShowMinimap),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.showOverlayInfo,
|
||||
onChanged: (v) => settings.showOverlayInfo = v,
|
||||
title: Text(context.l10n.settingsViewerShowInformation),
|
||||
subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.showOverlayShootingDetails,
|
||||
onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null,
|
||||
title: Text(context.l10n.settingsViewerShowShootingDetails),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsRasterImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.rasterBackground,
|
||||
setter: (value) => settings.rasterBackground = value,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVectorImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => settings.vectorBackground,
|
||||
setter: (value) => settings.vectorBackground = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoSection(BuildContext context) {
|
||||
final hiddenFilters = settings.hiddenFilters;
|
||||
final showVideos = !hiddenFilters.contains(MimeFilter.video);
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.video, stringToColor('Video')),
|
||||
title: context.l10n.settingsSectionVideo,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: showVideos,
|
||||
onChanged: (v) => context.read<CollectionSource>().changeFilterVisibility(MimeFilter.video, v),
|
||||
title: Text(context.l10n.settingsVideoShowVideos),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.enableVideoHardwareAcceleration,
|
||||
onChanged: (v) => settings.enableVideoHardwareAcceleration = v,
|
||||
title: Text(context.l10n.settingsVideoEnableHardwareAcceleration),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.enableVideoAutoPlay,
|
||||
onChanged: (v) => settings.enableVideoAutoPlay = v,
|
||||
title: Text(context.l10n.settingsVideoEnableAutoPlay),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVideoLoopModeTile),
|
||||
subtitle: Text(settings.videoLoopMode.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<VideoLoopMode>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||
initialValue: settings.videoLoopMode,
|
||||
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsVideoLoopModeTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.videoLoopMode = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
VideoActionsTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPrivacySection(BuildContext context) {
|
||||
return AvesExpansionTile(
|
||||
leading: _buildLeading(AIcons.privacy, stringToColor('Privacy')),
|
||||
title: context.l10n.settingsSectionPrivacy,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: settings.isCrashlyticsEnabled,
|
||||
onChanged: (v) => settings.isCrashlyticsEnabled = v,
|
||||
title: Text(context.l10n.settingsEnableAnalytics),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: settings.saveSearchHistory,
|
||||
onChanged: (v) {
|
||||
settings.saveSearchHistory = v;
|
||||
if (!v) {
|
||||
settings.searchHistory = [];
|
||||
}
|
||||
},
|
||||
title: Text(context.l10n.settingsSaveSearchHistory),
|
||||
),
|
||||
HiddenFilterTile(),
|
||||
StorageAccessTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageSection(BuildContext context) {
|
||||
return AvesExpansionTile(
|
||||
// use a fixed value instead of the title to identify this expansion tile
|
||||
// so that the tile state is kept when the language is modified
|
||||
value: 'language',
|
||||
leading: _buildLeading(AIcons.language, stringToColor('Language')),
|
||||
title: context.l10n.settingsSectionLanguage,
|
||||
expandedNotifier: _expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
LanguageTile(),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsCoordinateFormatTile),
|
||||
subtitle: Text(settings.coordinateFormat.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<CoordinateFormat>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||
initialValue: settings.coordinateFormat,
|
||||
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
optionSubtitleBuilder: (value) => value.format(Constants.pointNemo),
|
||||
title: context.l10n.settingsCoordinateFormatTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.coordinateFormat = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeading(IconData icon, Color color) => Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: color,
|
||||
width: AvesFilterChip.outlineWidth,
|
||||
)),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: DecoratedIcon(
|
||||
icon,
|
||||
shadows: Constants.embossShadows,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
79
lib/widgets/settings/thumbnails.dart
Normal file
79
lib/widgets/settings/thumbnails.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ThumbnailsSection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const ThumbnailsSection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentShowThumbnailLocation = context.select<Settings, bool>((s) => s.showThumbnailLocation);
|
||||
final currentShowThumbnailRaw = context.select<Settings, bool>((s) => s.showThumbnailRaw);
|
||||
final currentShowThumbnailVideoDuration = context.select<Settings, bool>((s) => s.showThumbnailVideoDuration);
|
||||
|
||||
final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor;
|
||||
double opacityFor(bool enabled) => enabled ? 1 : .2;
|
||||
|
||||
return AvesExpansionTile(
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.grid,
|
||||
color: stringToColor('Thumbnails'),
|
||||
),
|
||||
title: context.l10n.settingsSectionThumbnails,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: currentShowThumbnailLocation,
|
||||
onChanged: (v) => settings.showThumbnailLocation = v,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(currentShowThumbnailLocation),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.location,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentShowThumbnailRaw,
|
||||
onChanged: (v) => settings.showThumbnailRaw = v,
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
|
||||
AnimatedOpacity(
|
||||
opacity: opacityFor(currentShowThumbnailRaw),
|
||||
duration: Durations.toggleableTransitionAnimation,
|
||||
child: Icon(
|
||||
AIcons.raw,
|
||||
size: iconSize,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentShowThumbnailVideoDuration,
|
||||
onChanged: (v) => settings.showThumbnailVideoDuration = v,
|
||||
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
76
lib/widgets/settings/video/video.dart
Normal file
76
lib/widgets/settings/video/video.dart
Normal file
|
@ -0,0 +1,76 @@
|
|||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/settings/video_loop_mode.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/video/video_actions_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VideoSection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const VideoSection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentShowVideos = context.select<Settings, bool>((s) => !s.hiddenFilters.contains(MimeFilter.video));
|
||||
final currentEnableVideoHardwareAcceleration = context.select<Settings, bool>((s) => s.enableVideoHardwareAcceleration);
|
||||
final currentEnableVideoAutoPlay = context.select<Settings, bool>((s) => s.enableVideoAutoPlay);
|
||||
final currentVideoLoopMode = context.select<Settings, VideoLoopMode>((s) => s.videoLoopMode);
|
||||
|
||||
return AvesExpansionTile(
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.video,
|
||||
color: stringToColor('Video'),
|
||||
),
|
||||
title: context.l10n.settingsSectionVideo,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
value: currentShowVideos,
|
||||
onChanged: (v) => context.read<CollectionSource>().changeFilterVisibility(MimeFilter.video, v),
|
||||
title: Text(context.l10n.settingsVideoShowVideos),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentEnableVideoHardwareAcceleration,
|
||||
onChanged: (v) => settings.enableVideoHardwareAcceleration = v,
|
||||
title: Text(context.l10n.settingsVideoEnableHardwareAcceleration),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentEnableVideoAutoPlay,
|
||||
onChanged: (v) => settings.enableVideoAutoPlay = v,
|
||||
title: Text(context.l10n.settingsVideoEnableAutoPlay),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVideoLoopModeTile),
|
||||
subtitle: Text(currentVideoLoopMode.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<VideoLoopMode>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||
initialValue: currentVideoLoopMode,
|
||||
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsVideoLoopModeTitle,
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.videoLoopMode = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
VideoActionsTile(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/editor_page.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoActionsTile extends StatelessWidget {
|
72
lib/widgets/settings/viewer/viewer.dart
Normal file
72
lib/widgets/settings/viewer/viewer.dart
Normal file
|
@ -0,0 +1,72 @@
|
|||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/viewer/entry_background.dart';
|
||||
import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ViewerSection extends StatelessWidget {
|
||||
final ValueNotifier<String?> expandedNotifier;
|
||||
|
||||
const ViewerSection({
|
||||
Key? key,
|
||||
required this.expandedNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentShowOverlayMinimap = context.select<Settings, bool>((s) => s.showOverlayMinimap);
|
||||
final currentShowOverlayInfo = context.select<Settings, bool>((s) => s.showOverlayInfo);
|
||||
final currentShowOverlayShootingDetails = context.select<Settings, bool>((s) => s.showOverlayShootingDetails);
|
||||
final currentRasterBackground = context.select<Settings, EntryBackground>((s) => s.rasterBackground);
|
||||
final currentVectorBackground = context.select<Settings, EntryBackground>((s) => s.vectorBackground);
|
||||
|
||||
return AvesExpansionTile(
|
||||
leading: SettingsTileLeading(
|
||||
icon: AIcons.image,
|
||||
color: stringToColor('Image'),
|
||||
),
|
||||
title: context.l10n.settingsSectionViewer,
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: [
|
||||
ViewerActionsTile(),
|
||||
SwitchListTile(
|
||||
value: currentShowOverlayMinimap,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
title: Text(context.l10n.settingsViewerShowMinimap),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentShowOverlayInfo,
|
||||
onChanged: (v) => settings.showOverlayInfo = v,
|
||||
title: Text(context.l10n.settingsViewerShowInformation),
|
||||
subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle),
|
||||
),
|
||||
SwitchListTile(
|
||||
value: currentShowOverlayShootingDetails,
|
||||
onChanged: currentShowOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null,
|
||||
title: Text(context.l10n.settingsViewerShowShootingDetails),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsRasterImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => currentRasterBackground,
|
||||
setter: (value) => settings.rasterBackground = value,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.settingsVectorImageBackground),
|
||||
trailing: EntryBackgroundSelector(
|
||||
getter: () => currentVectorBackground,
|
||||
setter: (value) => settings.vectorBackground = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/quick_actions/editor_page.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ViewerActionsTile extends StatelessWidget {
|
|
@ -40,7 +40,7 @@ class AssParser {
|
|||
|
||||
static const noBreakSpace = '\u00A0';
|
||||
|
||||
// Parse text with ASS format tags
|
||||
// Parse text with ASS style overrides
|
||||
// cf https://aegi.vmoe.info/docs/3.0/ASS_Tags/
|
||||
// e.g. `And I'm like, "We can't {\i1}not{\i0} see it."`
|
||||
// e.g. `{\fad(200,200)\blur3}lorem ipsum"`
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/utils/math_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/outlined_text.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
|
@ -15,16 +16,13 @@ class VideoSubtitles extends StatelessWidget {
|
|||
final ValueNotifier<ViewState> viewStateNotifier;
|
||||
final bool debugMode;
|
||||
|
||||
static const baseStyle = TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
offset: Offset(1, 1),
|
||||
),
|
||||
],
|
||||
);
|
||||
static const baseOutlineColor = Colors.black;
|
||||
static const baseShadows = [
|
||||
Shadow(
|
||||
color: Colors.black54,
|
||||
offset: Offset(1, 1),
|
||||
),
|
||||
];
|
||||
|
||||
const VideoSubtitles({
|
||||
Key? key,
|
||||
|
@ -37,209 +35,223 @@ class VideoSubtitles extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value);
|
||||
return IgnorePointer(
|
||||
child: Selector<MediaQueryData, Orientation>(
|
||||
selector: (c, mq) => mq.orientation,
|
||||
builder: (c, orientation, child) {
|
||||
final bottom = orientation == Orientation.portrait ? .5 : .8;
|
||||
Alignment toVerticalAlignment(SubtitleStyle extraStyle) {
|
||||
switch (extraStyle.vAlign) {
|
||||
case TextAlignVertical.top:
|
||||
return Alignment(0, -bottom);
|
||||
case TextAlignVertical.center:
|
||||
return Alignment.center;
|
||||
case TextAlignVertical.bottom:
|
||||
default:
|
||||
return Alignment(0, bottom);
|
||||
}
|
||||
}
|
||||
child: Consumer<Settings>(
|
||||
builder: (context, settings, child) {
|
||||
final baseTextAlign = settings.subtitleTextAlignment;
|
||||
final baseOutlineWidth = settings.subtitleShowOutline ? 1 : 0;
|
||||
final baseStyle = TextStyle(
|
||||
color: settings.subtitleTextColor,
|
||||
backgroundColor: settings.subtitleBackgroundColor,
|
||||
fontSize: settings.subtitleFontSize,
|
||||
shadows: settings.subtitleShowOutline ? baseShadows : null,
|
||||
);
|
||||
|
||||
final viewportSize = context.read<MediaQueryData>().size;
|
||||
return Selector<MediaQueryData, Orientation>(
|
||||
selector: (c, mq) => mq.orientation,
|
||||
builder: (c, orientation, child) {
|
||||
final bottom = orientation == Orientation.portrait ? .5 : .8;
|
||||
Alignment toVerticalAlignment(SubtitleStyle extraStyle) {
|
||||
switch (extraStyle.vAlign) {
|
||||
case TextAlignVertical.top:
|
||||
return Alignment(0, -bottom);
|
||||
case TextAlignVertical.center:
|
||||
return Alignment.center;
|
||||
case TextAlignVertical.bottom:
|
||||
default:
|
||||
return Alignment(0, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<ViewState>(
|
||||
valueListenable: viewStateNotifier,
|
||||
builder: (context, viewState, child) {
|
||||
final viewPosition = viewState.position;
|
||||
final viewScale = viewState.scale ?? 1;
|
||||
final viewSize = videoDisplaySize * viewScale;
|
||||
final viewOffset = Offset(
|
||||
(viewportSize.width - viewSize.width) / 2,
|
||||
(viewportSize.height - viewSize.height) / 2,
|
||||
);
|
||||
final viewportSize = context.read<MediaQueryData>().size;
|
||||
|
||||
return StreamBuilder<String?>(
|
||||
stream: controller.timedTextStream,
|
||||
builder: (context, snapshot) {
|
||||
final text = snapshot.data;
|
||||
if (text == null) return const SizedBox();
|
||||
return ValueListenableBuilder<ViewState>(
|
||||
valueListenable: viewStateNotifier,
|
||||
builder: (context, viewState, child) {
|
||||
final viewPosition = viewState.position;
|
||||
final viewScale = viewState.scale ?? 1;
|
||||
final viewSize = videoDisplaySize * viewScale;
|
||||
final viewOffset = Offset(
|
||||
(viewportSize.width - viewSize.width) / 2,
|
||||
(viewportSize.height - viewSize.height) / 2,
|
||||
);
|
||||
|
||||
if (debugMode) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 100.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: OutlinedText(
|
||||
textSpans: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
)
|
||||
],
|
||||
outlineWidth: 1,
|
||||
outlineColor: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return StreamBuilder<String?>(
|
||||
stream: controller.timedTextStream,
|
||||
builder: (context, snapshot) {
|
||||
final text = snapshot.data;
|
||||
if (text == null) return const SizedBox();
|
||||
|
||||
final styledLine = AssParser.parse(text, baseStyle, viewScale);
|
||||
final position = styledLine.position;
|
||||
final clip = styledLine.clip;
|
||||
final styledSpans = styledLine.spans;
|
||||
final byExtraStyle = groupBy<StyledSubtitleSpan, SubtitleStyle>(styledSpans, (v) => v.extraStyle);
|
||||
return Stack(
|
||||
children: byExtraStyle.entries.map((kv) {
|
||||
final extraStyle = kv.key;
|
||||
final spans = kv.value.map((v) {
|
||||
final span = v.textSpan;
|
||||
final style = span.style;
|
||||
if (position == null || style == null) return span;
|
||||
|
||||
final letterSpacing = style.letterSpacing;
|
||||
final shadows = style.shadows;
|
||||
return TextSpan(
|
||||
text: span.text,
|
||||
style: style.copyWith(
|
||||
letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null,
|
||||
shadows: shadows != null
|
||||
? shadows
|
||||
.map((v) => Shadow(
|
||||
color: v.color,
|
||||
offset: v.offset * viewScale,
|
||||
blurRadius: v.blurRadius * viewScale,
|
||||
))
|
||||
.toList()
|
||||
: null,
|
||||
if (debugMode) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 100.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: OutlinedText(
|
||||
textSpans: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
)
|
||||
],
|
||||
outlineWidth: 1,
|
||||
outlineColor: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
final drawingPaths = extraStyle.drawingPaths;
|
||||
|
||||
Widget child;
|
||||
if (drawingPaths != null) {
|
||||
child = CustomPaint(
|
||||
painter: SubtitlePathPainter(
|
||||
paths: drawingPaths,
|
||||
scale: viewScale,
|
||||
fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
|
||||
strokeColor: extraStyle.borderColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
|
||||
child = OutlinedText(
|
||||
textSpans: spans,
|
||||
outlineWidth: outlineWidth * (position != null ? viewScale : 1),
|
||||
outlineColor: extraStyle.borderColor ?? Colors.black,
|
||||
outlineBlurSigma: extraStyle.edgeBlur ?? 0,
|
||||
textAlign: extraStyle.hAlign ?? TextAlign.center,
|
||||
);
|
||||
}
|
||||
|
||||
var transform = Matrix4.identity();
|
||||
final styledLine = AssParser.parse(text, baseStyle, viewScale);
|
||||
final position = styledLine.position;
|
||||
final clip = styledLine.clip;
|
||||
final styledSpans = styledLine.spans;
|
||||
final byExtraStyle = groupBy<StyledSubtitleSpan, SubtitleStyle>(styledSpans, (v) => v.extraStyle);
|
||||
return Stack(
|
||||
children: byExtraStyle.entries.map((kv) {
|
||||
final extraStyle = kv.key;
|
||||
final spans = kv.value.map((v) {
|
||||
final span = v.textSpan;
|
||||
final style = span.style;
|
||||
if (position == null || style == null) return span;
|
||||
|
||||
if (position != null) {
|
||||
final para = RenderParagraph(
|
||||
TextSpan(children: spans),
|
||||
textDirection: TextDirection.ltr,
|
||||
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
|
||||
)..layout(const BoxConstraints());
|
||||
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
|
||||
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
|
||||
final letterSpacing = style.letterSpacing;
|
||||
final shadows = style.shadows;
|
||||
return TextSpan(
|
||||
text: span.text,
|
||||
style: style.copyWith(
|
||||
letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null,
|
||||
shadows: shadows != null
|
||||
? shadows
|
||||
.map((v) => Shadow(
|
||||
color: v.color,
|
||||
offset: v.offset * viewScale,
|
||||
blurRadius: v.blurRadius * viewScale,
|
||||
))
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
final drawingPaths = extraStyle.drawingPaths;
|
||||
|
||||
late double anchorOffsetX, anchorOffsetY;
|
||||
switch (extraStyle.hAlign) {
|
||||
case TextAlign.left:
|
||||
anchorOffsetX = 0;
|
||||
break;
|
||||
case TextAlign.right:
|
||||
anchorOffsetX = -textWidth;
|
||||
break;
|
||||
case TextAlign.center:
|
||||
default:
|
||||
anchorOffsetX = -textWidth / 2;
|
||||
break;
|
||||
}
|
||||
switch (extraStyle.vAlign) {
|
||||
case TextAlignVertical.top:
|
||||
anchorOffsetY = 0;
|
||||
break;
|
||||
case TextAlignVertical.center:
|
||||
anchorOffsetY = -textHeight / 2;
|
||||
break;
|
||||
case TextAlignVertical.bottom:
|
||||
default:
|
||||
anchorOffsetY = -textHeight;
|
||||
break;
|
||||
}
|
||||
final alignOffset = Offset(anchorOffsetX, anchorOffsetY);
|
||||
final lineOffset = position * viewScale + viewPosition;
|
||||
final translateOffset = viewOffset + lineOffset + alignOffset;
|
||||
transform.translate(translateOffset.dx, translateOffset.dy);
|
||||
}
|
||||
Widget child;
|
||||
if (drawingPaths != null) {
|
||||
child = CustomPaint(
|
||||
painter: SubtitlePathPainter(
|
||||
paths: drawingPaths,
|
||||
scale: viewScale,
|
||||
fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
|
||||
strokeColor: extraStyle.borderColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
|
||||
child = OutlinedText(
|
||||
textSpans: spans,
|
||||
outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth),
|
||||
outlineColor: extraStyle.borderColor ?? baseOutlineColor,
|
||||
outlineBlurSigma: extraStyle.edgeBlur ?? 0,
|
||||
textAlign: extraStyle.hAlign ?? baseTextAlign,
|
||||
);
|
||||
}
|
||||
|
||||
if (extraStyle.rotating) {
|
||||
// for perspective
|
||||
transform.setEntry(3, 2, 0.001);
|
||||
final x = -toRadians(extraStyle.rotationX ?? 0);
|
||||
final y = -toRadians(extraStyle.rotationY ?? 0);
|
||||
final z = -toRadians(extraStyle.rotationZ ?? 0);
|
||||
if (x != 0) transform.rotateX(x);
|
||||
if (y != 0) transform.rotateY(y);
|
||||
if (z != 0) transform.rotateZ(z);
|
||||
}
|
||||
if (extraStyle.scaling) {
|
||||
final x = extraStyle.scaleX ?? 1;
|
||||
final y = extraStyle.scaleY ?? 1;
|
||||
transform.scale(x, y);
|
||||
}
|
||||
if (extraStyle.shearing) {
|
||||
final x = extraStyle.shearX ?? 0;
|
||||
final y = extraStyle.shearY ?? 0;
|
||||
transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
|
||||
}
|
||||
var transform = Matrix4.identity();
|
||||
|
||||
if (!transform.isIdentity()) {
|
||||
child = Transform(
|
||||
transform: transform,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
if (position != null) {
|
||||
final para = RenderParagraph(
|
||||
TextSpan(children: spans),
|
||||
textDirection: TextDirection.ltr,
|
||||
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
|
||||
)..layout(const BoxConstraints());
|
||||
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
|
||||
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
|
||||
|
||||
if (position == null) {
|
||||
child = Align(
|
||||
alignment: toVerticalAlignment(extraStyle),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
late double anchorOffsetX, anchorOffsetY;
|
||||
switch (extraStyle.hAlign ?? baseTextAlign) {
|
||||
case TextAlign.left:
|
||||
anchorOffsetX = 0;
|
||||
break;
|
||||
case TextAlign.right:
|
||||
anchorOffsetX = -textWidth;
|
||||
break;
|
||||
case TextAlign.center:
|
||||
case TextAlign.start:
|
||||
case TextAlign.end:
|
||||
case TextAlign.justify:
|
||||
anchorOffsetX = -textWidth / 2;
|
||||
break;
|
||||
}
|
||||
switch (extraStyle.vAlign ?? TextAlignVertical.bottom) {
|
||||
case TextAlignVertical.top:
|
||||
anchorOffsetY = 0;
|
||||
break;
|
||||
case TextAlignVertical.center:
|
||||
anchorOffsetY = -textHeight / 2;
|
||||
break;
|
||||
case TextAlignVertical.bottom:
|
||||
anchorOffsetY = -textHeight;
|
||||
break;
|
||||
}
|
||||
final alignOffset = Offset(anchorOffsetX, anchorOffsetY);
|
||||
final lineOffset = position * viewScale + viewPosition;
|
||||
final translateOffset = viewOffset + lineOffset + alignOffset;
|
||||
transform.translate(translateOffset.dx, translateOffset.dy);
|
||||
}
|
||||
|
||||
if (clip != null) {
|
||||
final clipOffset = viewOffset + viewPosition;
|
||||
final matrix = Matrix4.identity()
|
||||
..translate(clipOffset.dx, clipOffset.dy)
|
||||
..scale(viewScale, viewScale);
|
||||
final transform = matrix.storage;
|
||||
child = ClipPath(
|
||||
clipper: SubtitlePathClipper(
|
||||
paths: clip.map((v) => v.transform(transform)).toList(),
|
||||
scale: viewScale,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
if (extraStyle.rotating) {
|
||||
// for perspective
|
||||
transform.setEntry(3, 2, 0.001);
|
||||
final x = -toRadians(extraStyle.rotationX ?? 0);
|
||||
final y = -toRadians(extraStyle.rotationY ?? 0);
|
||||
final z = -toRadians(extraStyle.rotationZ ?? 0);
|
||||
if (x != 0) transform.rotateX(x);
|
||||
if (y != 0) transform.rotateY(y);
|
||||
if (z != 0) transform.rotateZ(z);
|
||||
}
|
||||
if (extraStyle.scaling) {
|
||||
final x = extraStyle.scaleX ?? 1;
|
||||
final y = extraStyle.scaleY ?? 1;
|
||||
transform.scale(x, y);
|
||||
}
|
||||
if (extraStyle.shearing) {
|
||||
final x = extraStyle.shearX ?? 0;
|
||||
final y = extraStyle.shearY ?? 0;
|
||||
transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
|
||||
}
|
||||
|
||||
return child;
|
||||
}).toList(),
|
||||
if (!transform.isIdentity()) {
|
||||
child = Transform(
|
||||
transform: transform,
|
||||
alignment: Alignment.center,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (position == null) {
|
||||
child = Align(
|
||||
alignment: toVerticalAlignment(extraStyle),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (clip != null) {
|
||||
final clipOffset = viewOffset + viewPosition;
|
||||
final matrix = Matrix4.identity()
|
||||
..translate(clipOffset.dx, clipOffset.dy)
|
||||
..scale(viewScale, viewScale);
|
||||
final transform = matrix.storage;
|
||||
child = ClipPath(
|
||||
clipper: SubtitlePathClipper(
|
||||
paths: clip.map((v) => v.transform(transform)).toList(),
|
||||
scale: viewScale,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue