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": "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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -27,22 +27,23 @@ class OutlinedText extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
ImageFiltered(
|
if (outlineWidth > 0)
|
||||||
imageFilter: outlineBlurSigma > 0
|
ImageFiltered(
|
||||||
? ImageFilter.blur(
|
imageFilter: outlineBlurSigma > 0
|
||||||
sigmaX: outlineBlurSigma,
|
? ImageFilter.blur(
|
||||||
sigmaY: outlineBlurSigma,
|
sigmaX: outlineBlurSigma,
|
||||||
)
|
sigmaY: outlineBlurSigma,
|
||||||
: ImageFilter.matrix(
|
)
|
||||||
Matrix4.identity().storage,
|
: ImageFilter.matrix(
|
||||||
),
|
Matrix4.identity().storage,
|
||||||
child: Text.rich(
|
),
|
||||||
TextSpan(
|
child: Text.rich(
|
||||||
children: textSpans.map(_toStrokeSpan).toList(),
|
TextSpan(
|
||||||
|
children: textSpans.map(_toStrokeSpan).toList(),
|
||||||
|
),
|
||||||
|
textAlign: textAlign,
|
||||||
),
|
),
|
||||||
textAlign: textAlign,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Text.rich(
|
Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
children: textSpans,
|
children: textSpans,
|
||||||
|
|
|
@ -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 {
|
|
@ -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';
|
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: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
|
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/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,
|
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/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,28 +36,26 @@ 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(
|
duration: Durations.staggeredAnimation,
|
||||||
duration: Durations.staggeredAnimation,
|
delay: Durations.staggeredAnimationDelay,
|
||||||
delay: Durations.staggeredAnimationDelay,
|
childAnimationBuilder: (child) => SlideAnimation(
|
||||||
childAnimationBuilder: (child) => SlideAnimation(
|
verticalOffset: 50.0,
|
||||||
verticalOffset: 50.0,
|
child: FadeInAnimation(
|
||||||
child: FadeInAnimation(
|
child: child,
|
||||||
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/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 {
|
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/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 {
|
|
@ -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"`
|
||||||
|
|
|
@ -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,
|
Shadow(
|
||||||
shadows: [
|
color: Colors.black54,
|
||||||
Shadow(
|
offset: Offset(1, 1),
|
||||||
color: Colors.black54,
|
),
|
||||||
offset: Offset(1, 1),
|
];
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const VideoSubtitles({
|
const VideoSubtitles({
|
||||||
Key? key,
|
Key? key,
|
||||||
|
@ -37,209 +35,223 @@ 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>(
|
||||||
selector: (c, mq) => mq.orientation,
|
builder: (context, settings, child) {
|
||||||
builder: (c, orientation, child) {
|
final baseTextAlign = settings.subtitleTextAlignment;
|
||||||
final bottom = orientation == Orientation.portrait ? .5 : .8;
|
final baseOutlineWidth = settings.subtitleShowOutline ? 1 : 0;
|
||||||
Alignment toVerticalAlignment(SubtitleStyle extraStyle) {
|
final baseStyle = TextStyle(
|
||||||
switch (extraStyle.vAlign) {
|
color: settings.subtitleTextColor,
|
||||||
case TextAlignVertical.top:
|
backgroundColor: settings.subtitleBackgroundColor,
|
||||||
return Alignment(0, -bottom);
|
fontSize: settings.subtitleFontSize,
|
||||||
case TextAlignVertical.center:
|
shadows: settings.subtitleShowOutline ? baseShadows : null,
|
||||||
return Alignment.center;
|
);
|
||||||
case TextAlignVertical.bottom:
|
|
||||||
default:
|
|
||||||
return Alignment(0, bottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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>(
|
final viewportSize = context.read<MediaQueryData>().size;
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
return StreamBuilder<String?>(
|
return ValueListenableBuilder<ViewState>(
|
||||||
stream: controller.timedTextStream,
|
valueListenable: viewStateNotifier,
|
||||||
builder: (context, snapshot) {
|
builder: (context, viewState, child) {
|
||||||
final text = snapshot.data;
|
final viewPosition = viewState.position;
|
||||||
if (text == null) return const SizedBox();
|
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 StreamBuilder<String?>(
|
||||||
return Padding(
|
stream: controller.timedTextStream,
|
||||||
padding: const EdgeInsets.only(top: 100.0),
|
builder: (context, snapshot) {
|
||||||
child: Align(
|
final text = snapshot.data;
|
||||||
alignment: Alignment.topLeft,
|
if (text == null) return const SizedBox();
|
||||||
child: OutlinedText(
|
|
||||||
textSpans: [
|
|
||||||
TextSpan(
|
|
||||||
text: text,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
outlineWidth: 1,
|
|
||||||
outlineColor: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final styledLine = AssParser.parse(text, baseStyle, viewScale);
|
if (debugMode) {
|
||||||
final position = styledLine.position;
|
return Padding(
|
||||||
final clip = styledLine.clip;
|
padding: const EdgeInsets.only(top: 100.0),
|
||||||
final styledSpans = styledLine.spans;
|
child: Align(
|
||||||
final byExtraStyle = groupBy<StyledSubtitleSpan, SubtitleStyle>(styledSpans, (v) => v.extraStyle);
|
alignment: Alignment.topLeft,
|
||||||
return Stack(
|
child: OutlinedText(
|
||||||
children: byExtraStyle.entries.map((kv) {
|
textSpans: [
|
||||||
final extraStyle = kv.key;
|
TextSpan(
|
||||||
final spans = kv.value.map((v) {
|
text: text,
|
||||||
final span = v.textSpan;
|
style: const TextStyle(fontSize: 14),
|
||||||
final style = span.style;
|
)
|
||||||
if (position == null || style == null) return span;
|
],
|
||||||
|
outlineWidth: 1,
|
||||||
final letterSpacing = style.letterSpacing;
|
outlineColor: Colors.black,
|
||||||
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;
|
|
||||||
|
|
||||||
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 letterSpacing = style.letterSpacing;
|
||||||
final para = RenderParagraph(
|
final shadows = style.shadows;
|
||||||
TextSpan(children: spans),
|
return TextSpan(
|
||||||
textDirection: TextDirection.ltr,
|
text: span.text,
|
||||||
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
|
style: style.copyWith(
|
||||||
)..layout(const BoxConstraints());
|
letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null,
|
||||||
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
|
shadows: shadows != null
|
||||||
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
|
? 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;
|
Widget child;
|
||||||
switch (extraStyle.hAlign) {
|
if (drawingPaths != null) {
|
||||||
case TextAlign.left:
|
child = CustomPaint(
|
||||||
anchorOffsetX = 0;
|
painter: SubtitlePathPainter(
|
||||||
break;
|
paths: drawingPaths,
|
||||||
case TextAlign.right:
|
scale: viewScale,
|
||||||
anchorOffsetX = -textWidth;
|
fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
|
||||||
break;
|
strokeColor: extraStyle.borderColor,
|
||||||
case TextAlign.center:
|
),
|
||||||
default:
|
);
|
||||||
anchorOffsetX = -textWidth / 2;
|
} else {
|
||||||
break;
|
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
|
||||||
}
|
child = OutlinedText(
|
||||||
switch (extraStyle.vAlign) {
|
textSpans: spans,
|
||||||
case TextAlignVertical.top:
|
outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth),
|
||||||
anchorOffsetY = 0;
|
outlineColor: extraStyle.borderColor ?? baseOutlineColor,
|
||||||
break;
|
outlineBlurSigma: extraStyle.edgeBlur ?? 0,
|
||||||
case TextAlignVertical.center:
|
textAlign: extraStyle.hAlign ?? baseTextAlign,
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraStyle.rotating) {
|
var transform = Matrix4.identity();
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transform.isIdentity()) {
|
if (position != null) {
|
||||||
child = Transform(
|
final para = RenderParagraph(
|
||||||
transform: transform,
|
TextSpan(children: spans),
|
||||||
alignment: Alignment.center,
|
textDirection: TextDirection.ltr,
|
||||||
child: child,
|
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
|
||||||
);
|
)..layout(const BoxConstraints());
|
||||||
}
|
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
|
||||||
|
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
|
||||||
|
|
||||||
if (position == null) {
|
late double anchorOffsetX, anchorOffsetY;
|
||||||
child = Align(
|
switch (extraStyle.hAlign ?? baseTextAlign) {
|
||||||
alignment: toVerticalAlignment(extraStyle),
|
case TextAlign.left:
|
||||||
child: child,
|
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) {
|
if (extraStyle.rotating) {
|
||||||
final clipOffset = viewOffset + viewPosition;
|
// for perspective
|
||||||
final matrix = Matrix4.identity()
|
transform.setEntry(3, 2, 0.001);
|
||||||
..translate(clipOffset.dx, clipOffset.dy)
|
final x = -toRadians(extraStyle.rotationX ?? 0);
|
||||||
..scale(viewScale, viewScale);
|
final y = -toRadians(extraStyle.rotationY ?? 0);
|
||||||
final transform = matrix.storage;
|
final z = -toRadians(extraStyle.rotationZ ?? 0);
|
||||||
child = ClipPath(
|
if (x != 0) transform.rotateX(x);
|
||||||
clipper: SubtitlePathClipper(
|
if (y != 0) transform.rotateY(y);
|
||||||
paths: clip.map((v) => v.transform(transform)).toList(),
|
if (z != 0) transform.rotateZ(z);
|
||||||
scale: viewScale,
|
}
|
||||||
),
|
if (extraStyle.scaling) {
|
||||||
child: child,
|
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;
|
if (!transform.isIdentity()) {
|
||||||
}).toList(),
|
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