improved settings rebuild

This commit is contained in:
Thibault Deckers 2021-06-24 11:14:54 +09:00
parent 45153e94bb
commit 4345f46cc2
26 changed files with 771 additions and 540 deletions

View file

@ -597,6 +597,23 @@
"settingsVideoQuickActionEditorTitle": "Quick Video Actions", "settingsVideoQuickActionEditorTitle": "Quick Video Actions",
"@settingsVideoQuickActionEditorTitle": {}, "@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": "Privacy",
"@settingsSectionPrivacy": {}, "@settingsSectionPrivacy": {},
"settingsEnableAnalytics": "Allow anonymous analytics and crash reporting", "settingsEnableAnalytics": "Allow anonymous analytics and crash reporting",

View file

@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -57,6 +58,13 @@ class Settings extends ChangeNotifier {
static const videoLoopModeKey = 'video_loop'; static const videoLoopModeKey = 'video_loop';
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text'; 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 // info
static const infoMapStyleKey = 'info_map_style'; static const infoMapStyleKey = 'info_map_style';
static const infoMapZoomKey = 'info_map_zoom'; static const infoMapZoomKey = 'info_map_zoom';
@ -241,21 +249,43 @@ class Settings extends ChangeNotifier {
// video // video
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true); 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); bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false);
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values);
set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString()); set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString());
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue); 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 // info

View file

@ -52,6 +52,7 @@ class Durations {
// settings animations // settings animations
static const quickActionListAnimation = Duration(milliseconds: 200); static const quickActionListAnimation = Duration(milliseconds: 200);
static const quickActionHighlightAnimation = Duration(milliseconds: 200); static const quickActionHighlightAnimation = Duration(milliseconds: 200);
static const themeChangeDuration = Duration(milliseconds: 400);
// delays & refresh intervals // delays & refresh intervals
static const opToastDisplay = Duration(seconds: 3); static const opToastDisplay = Duration(seconds: 3);

View file

@ -27,6 +27,7 @@ class OutlinedText extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: [ children: [
if (outlineWidth > 0)
ImageFiltered( ImageFiltered(
imageFilter: outlineBlurSigma > 0 imageFilter: outlineBlurSigma > 0
? ImageFilter.blur( ? ImageFilter.blur(

View file

@ -1,6 +1,6 @@
import 'package:aves/widgets/common/providers/media_query_data_provider.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/common/quick_actions/action_button.dart';
import 'package:aves/widgets/settings/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class AvailableActionPanel<T extends Object> extends StatelessWidget { class AvailableActionPanel<T extends Object> extends StatelessWidget {

View file

@ -6,11 +6,11 @@ import 'package:aves/utils/change_notifier.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.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/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/settings/quick_actions/action_button.dart'; import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
import 'package:aves/widgets/settings/quick_actions/action_panel.dart'; import 'package:aves/widgets/settings/common/quick_actions/action_panel.dart';
import 'package:aves/widgets/settings/quick_actions/available_actions.dart'; import 'package:aves/widgets/settings/common/quick_actions/available_actions.dart';
import 'package:aves/widgets/settings/quick_actions/placeholder.dart'; import 'package:aves/widgets/settings/common/quick_actions/placeholder.dart';
import 'package:aves/widgets/settings/quick_actions/quick_actions.dart'; import 'package:aves/widgets/settings/common/quick_actions/quick_actions.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';

View 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,
),
);
}
}

View 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;
}
},
),
],
);
}
}

View file

@ -7,10 +7,9 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageTile extends StatelessWidget { class LocaleTile extends StatelessWidget {
static const _systemLocaleOption = Locale('system'); static const _systemLocaleOption = Locale('system');
@override @override

View 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),
),
],
);
}
}

View file

@ -1,3 +1,4 @@
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
@ -52,16 +53,16 @@ class HiddenFilterPage extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Consumer<Settings>( child: Selector<Settings, Set<CollectionFilter>>(
builder: (context, settings, child) { selector: (context, s) => settings.hiddenFilters,
final hiddenFilters = settings.hiddenFilters; builder: (context, hiddenFilters, child) {
final filterList = hiddenFilters.toList()..sort();
if (hiddenFilters.isEmpty) { if (hiddenFilters.isEmpty) {
return EmptyContent( return EmptyContent(
icon: AIcons.hide, icon: AIcons.hide,
text: context.l10n.settingsHiddenFiltersEmpty, text: context.l10n.settingsHiddenFiltersEmpty,
); );
} }
final filterList = hiddenFilters.toList()..sort();
return Wrap( return Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,

View 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(),
],
);
}
}

View file

@ -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/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/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/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/language/language.dart';
import 'package:aves/widgets/settings/access_grants.dart'; import 'package:aves/widgets/settings/navigation.dart';
import 'package:aves/widgets/settings/entry_background.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart';
import 'package:aves/widgets/settings/hidden_filters.dart'; import 'package:aves/widgets/settings/thumbnails.dart';
import 'package:aves/widgets/settings/language.dart'; import 'package:aves/widgets/settings/video/video.dart';
import 'package:aves/widgets/settings/video_actions_editor.dart'; import 'package:aves/widgets/settings/viewer/viewer.dart';
import 'package:aves/widgets/settings/viewer_actions_editor.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
static const routeName = '/settings'; static const routeName = '/settings';
@ -52,8 +36,7 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
), ),
child: SafeArea( child: SafeArea(
child: Consumer<Settings>( child: AnimationLimiter(
builder: (context, settings, child) => AnimationLimiter(
child: ListView( child: ListView(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
children: AnimationConfiguration.toStaggeredList( children: AnimationConfiguration.toStaggeredList(
@ -66,12 +49,12 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
), ),
children: [ children: [
_buildNavigationSection(context), NavigationSection(expandedNotifier: _expandedNotifier),
_buildThumbnailsSection(context), ThumbnailsSection(expandedNotifier: _expandedNotifier),
_buildViewerSection(context), ViewerSection(expandedNotifier: _expandedNotifier),
_buildVideoSection(context), VideoSection(expandedNotifier: _expandedNotifier),
_buildPrivacySection(context), PrivacySection(expandedNotifier: _expandedNotifier),
_buildLanguageSection(context), LanguageSection(expandedNotifier: _expandedNotifier),
], ],
), ),
), ),
@ -79,274 +62,6 @@ 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,
),
);
} }

View 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),
),
],
);
}
}

View 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(),
],
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/actions/video_actions.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.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'; import 'package:flutter/material.dart';
class VideoActionsTile extends StatelessWidget { class VideoActionsTile extends StatelessWidget {

View 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,
),
),
],
);
}
}

View file

@ -1,7 +1,7 @@
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.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'; import 'package:flutter/material.dart';
class ViewerActionsTile extends StatelessWidget { class ViewerActionsTile extends StatelessWidget {

View file

@ -40,7 +40,7 @@ class AssParser {
static const noBreakSpace = '\u00A0'; 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/ // 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. `And I'm like, "We can't {\i1}not{\i0} see it."`
// e.g. `{\fad(200,200)\blur3}lorem ipsum"` // e.g. `{\fad(200,200)\blur3}lorem ipsum"`

View file

@ -1,3 +1,4 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:aves/widgets/common/basic/outlined_text.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
@ -15,16 +16,13 @@ class VideoSubtitles extends StatelessWidget {
final ValueNotifier<ViewState> viewStateNotifier; final ValueNotifier<ViewState> viewStateNotifier;
final bool debugMode; final bool debugMode;
static const baseStyle = TextStyle( static const baseOutlineColor = Colors.black;
color: Colors.white, static const baseShadows = [
fontSize: 20,
shadows: [
Shadow( Shadow(
color: Colors.black54, color: Colors.black54,
offset: Offset(1, 1), offset: Offset(1, 1),
), ),
], ];
);
const VideoSubtitles({ const VideoSubtitles({
Key? key, Key? key,
@ -37,7 +35,18 @@ class VideoSubtitles extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value); final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value);
return IgnorePointer( return IgnorePointer(
child: Selector<MediaQueryData, Orientation>( 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,
);
return Selector<MediaQueryData, Orientation>(
selector: (c, mq) => mq.orientation, selector: (c, mq) => mq.orientation,
builder: (c, orientation, child) { builder: (c, orientation, child) {
final bottom = orientation == Orientation.portrait ? .5 : .8; final bottom = orientation == Orientation.portrait ? .5 : .8;
@ -138,10 +147,10 @@ class VideoSubtitles extends StatelessWidget {
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1); final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
child = OutlinedText( child = OutlinedText(
textSpans: spans, textSpans: spans,
outlineWidth: outlineWidth * (position != null ? viewScale : 1), outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth),
outlineColor: extraStyle.borderColor ?? Colors.black, outlineColor: extraStyle.borderColor ?? baseOutlineColor,
outlineBlurSigma: extraStyle.edgeBlur ?? 0, outlineBlurSigma: extraStyle.edgeBlur ?? 0,
textAlign: extraStyle.hAlign ?? TextAlign.center, textAlign: extraStyle.hAlign ?? baseTextAlign,
); );
} }
@ -157,7 +166,7 @@ class VideoSubtitles extends StatelessWidget {
final textHeight = para.getMaxIntrinsicHeight(double.infinity); final textHeight = para.getMaxIntrinsicHeight(double.infinity);
late double anchorOffsetX, anchorOffsetY; late double anchorOffsetX, anchorOffsetY;
switch (extraStyle.hAlign) { switch (extraStyle.hAlign ?? baseTextAlign) {
case TextAlign.left: case TextAlign.left:
anchorOffsetX = 0; anchorOffsetX = 0;
break; break;
@ -165,11 +174,13 @@ class VideoSubtitles extends StatelessWidget {
anchorOffsetX = -textWidth; anchorOffsetX = -textWidth;
break; break;
case TextAlign.center: case TextAlign.center:
default: case TextAlign.start:
case TextAlign.end:
case TextAlign.justify:
anchorOffsetX = -textWidth / 2; anchorOffsetX = -textWidth / 2;
break; break;
} }
switch (extraStyle.vAlign) { switch (extraStyle.vAlign ?? TextAlignVertical.bottom) {
case TextAlignVertical.top: case TextAlignVertical.top:
anchorOffsetY = 0; anchorOffsetY = 0;
break; break;
@ -177,7 +188,6 @@ class VideoSubtitles extends StatelessWidget {
anchorOffsetY = -textHeight / 2; anchorOffsetY = -textHeight / 2;
break; break;
case TextAlignVertical.bottom: case TextAlignVertical.bottom:
default:
anchorOffsetY = -textHeight; anchorOffsetY = -textHeight;
break; break;
} }
@ -246,6 +256,8 @@ class VideoSubtitles extends StatelessWidget {
}, },
); );
}, },
);
},
), ),
); );
} }