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": {},
"settingsSubtitleThemeTile": "Subtitles",
"@settingsSubtitleThemeTile": {},
"settingsSubtitleThemeTitle": "Subtitles",
"@settingsSubtitleThemeTitle": {},
"settingsSubtitleThemeSample": "This is a sample.",
"@settingsSubtitleThemeSample": {},
"settingsSubtitleThemeTextAlignmentTile": "Text alignment",
"@settingsSubtitleThemeTextAlignmentTile": {},
"settingsSubtitleThemeTextAlignmentTitle": "Text Alignment",
"@settingsSubtitleThemeTextAlignmentTitle": {},
"settingsSubtitleThemeTextAlignmentLeft": "Left",
"@settingsSubtitleThemeTextAlignmentLeft": {},
"settingsSubtitleThemeTextAlignmentCenter": "Center",
"@settingsSubtitleThemeTextAlignmentCenter": {},
"settingsSubtitleThemeTextAlignmentRight": "Right",
"@settingsSubtitleThemeTextAlignmentRight": {},
"settingsSectionPrivacy": "Privacy",
"@settingsSectionPrivacy": {},
"settingsEnableAnalytics": "Allow anonymous analytics and crash reporting",

View file

@ -8,6 +8,7 @@ import 'package:collection/collection.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:pedantic/pedantic.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -57,6 +58,13 @@ class Settings extends ChangeNotifier {
static const videoLoopModeKey = 'video_loop';
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
// subtitles
static const subtitleFontSizeKey = 'subtitle_font_size';
static const subtitleTextAlignmentKey = 'subtitle_text_alignment';
static const subtitleShowOutlineKey = 'subtitle_show_outline';
static const subtitleTextColorKey = 'subtitle_text_color';
static const subtitleBackgroundColorKey = 'subtitle_background_color';
// info
static const infoMapStyleKey = 'info_map_style';
static const infoMapZoomKey = 'info_map_zoom';
@ -241,21 +249,43 @@ class Settings extends ChangeNotifier {
// video
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
bool get enableVideoHardwareAcceleration => getBoolOrDefault(enableVideoHardwareAccelerationKey, true);
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, false);
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values);
set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString());
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
bool get videoShowRawTimedText => getBoolOrDefault(videoShowRawTimedTextKey, false);
// subtitles
double get subtitleFontSize => _prefs!.getDouble(subtitleFontSizeKey) ?? 20;
set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue);
TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, TextAlign.center, TextAlign.values);
set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString());
bool get subtitleShowOutline => getBoolOrDefault(subtitleShowOutlineKey, true);
set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue);
Color get subtitleTextColor => Color(_prefs!.getInt(subtitleTextColorKey) ?? Colors.white.value);
set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value);
Color get subtitleBackgroundColor => Color(_prefs!.getInt(subtitleBackgroundColorKey) ?? Colors.transparent.value);
set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value);
// info

View file

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

View file

@ -27,22 +27,23 @@ class OutlinedText extends StatelessWidget {
Widget build(BuildContext context) {
return Stack(
children: [
ImageFiltered(
imageFilter: outlineBlurSigma > 0
? ImageFilter.blur(
sigmaX: outlineBlurSigma,
sigmaY: outlineBlurSigma,
)
: ImageFilter.matrix(
Matrix4.identity().storage,
),
child: Text.rich(
TextSpan(
children: textSpans.map(_toStrokeSpan).toList(),
if (outlineWidth > 0)
ImageFiltered(
imageFilter: outlineBlurSigma > 0
? ImageFilter.blur(
sigmaX: outlineBlurSigma,
sigmaY: outlineBlurSigma,
)
: ImageFilter.matrix(
Matrix4.identity().storage,
),
child: Text.rich(
TextSpan(
children: textSpans.map(_toStrokeSpan).toList(),
),
textAlign: textAlign,
),
textAlign: textAlign,
),
),
Text.rich(
TextSpan(
children: textSpans,

View file

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

View file

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

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:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageTile extends StatelessWidget {
class LocaleTile extends StatelessWidget {
static const _systemLocaleOption = Locale('system');
@override

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

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/icons.dart';
import 'package:aves/utils/color_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/settings/access_grants.dart';
import 'package:aves/widgets/settings/entry_background.dart';
import 'package:aves/widgets/settings/hidden_filters.dart';
import 'package:aves/widgets/settings/language.dart';
import 'package:aves/widgets/settings/video_actions_editor.dart';
import 'package:aves/widgets/settings/viewer_actions_editor.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:aves/widgets/settings/language/language.dart';
import 'package:aves/widgets/settings/navigation.dart';
import 'package:aves/widgets/settings/privacy/privacy.dart';
import 'package:aves/widgets/settings/thumbnails.dart';
import 'package:aves/widgets/settings/video/video.dart';
import 'package:aves/widgets/settings/viewer/viewer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
class SettingsPage extends StatefulWidget {
static const routeName = '/settings';
@ -52,28 +36,26 @@ class _SettingsPageState extends State<SettingsPage> {
),
),
child: SafeArea(
child: Consumer<Settings>(
builder: (context, settings, child) => AnimationLimiter(
child: ListView(
padding: const EdgeInsets.all(8),
children: AnimationConfiguration.toStaggeredList(
duration: Durations.staggeredAnimation,
delay: Durations.staggeredAnimationDelay,
childAnimationBuilder: (child) => SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: child,
),
child: AnimationLimiter(
child: ListView(
padding: const EdgeInsets.all(8),
children: AnimationConfiguration.toStaggeredList(
duration: Durations.staggeredAnimation,
delay: Durations.staggeredAnimationDelay,
childAnimationBuilder: (child) => SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: child,
),
children: [
_buildNavigationSection(context),
_buildThumbnailsSection(context),
_buildViewerSection(context),
_buildVideoSection(context),
_buildPrivacySection(context),
_buildLanguageSection(context),
],
),
children: [
NavigationSection(expandedNotifier: _expandedNotifier),
ThumbnailsSection(expandedNotifier: _expandedNotifier),
ViewerSection(expandedNotifier: _expandedNotifier),
VideoSection(expandedNotifier: _expandedNotifier),
PrivacySection(expandedNotifier: _expandedNotifier),
LanguageSection(expandedNotifier: _expandedNotifier),
],
),
),
),
@ -82,271 +64,4 @@ class _SettingsPageState extends State<SettingsPage> {
),
);
}
Widget _buildNavigationSection(BuildContext context) {
return AvesExpansionTile(
leading: _buildLeading(AIcons.home, stringToColor('Navigation')),
title: context.l10n.settingsSectionNavigation,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
ListTile(
title: Text(context.l10n.settingsHome),
subtitle: Text(settings.homePage.getName(context)),
onTap: () async {
final value = await showDialog<HomePageSetting>(
context: context,
builder: (context) => AvesSelectionDialog<HomePageSetting>(
initialValue: settings.homePage,
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.settingsHome,
),
);
if (value != null) {
settings.homePage = value;
}
},
),
ListTile(
title: Text(context.l10n.settingsKeepScreenOnTile),
subtitle: Text(settings.keepScreenOn.getName(context)),
onTap: () async {
final value = await showDialog<KeepScreenOn>(
context: context,
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
initialValue: settings.keepScreenOn,
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.settingsKeepScreenOnTitle,
),
);
if (value != null) {
settings.keepScreenOn = value;
}
},
),
SwitchListTile(
value: settings.mustBackTwiceToExit,
onChanged: (v) => settings.mustBackTwiceToExit = v,
title: Text(context.l10n.settingsDoubleBackExit),
),
],
);
}
Widget _buildThumbnailsSection(BuildContext context) {
final iconSize = IconTheme.of(context).size! * MediaQuery.of(context).textScaleFactor;
double opacityFor(bool enabled) => enabled ? 1 : .2;
return AvesExpansionTile(
leading: _buildLeading(AIcons.grid, stringToColor('Thumbnails')),
title: context.l10n.settingsSectionThumbnails,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
SwitchListTile(
value: settings.showThumbnailLocation,
onChanged: (v) => settings.showThumbnailLocation = v,
title: Row(
children: [
Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)),
AnimatedOpacity(
opacity: opacityFor(settings.showThumbnailLocation),
duration: Durations.toggleableTransitionAnimation,
child: Icon(
AIcons.location,
size: iconSize,
),
),
],
),
),
SwitchListTile(
value: settings.showThumbnailRaw,
onChanged: (v) => settings.showThumbnailRaw = v,
title: Row(
children: [
Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)),
AnimatedOpacity(
opacity: opacityFor(settings.showThumbnailRaw),
duration: Durations.toggleableTransitionAnimation,
child: Icon(
AIcons.raw,
size: iconSize,
),
),
],
),
),
SwitchListTile(
value: settings.showThumbnailVideoDuration,
onChanged: (v) => settings.showThumbnailVideoDuration = v,
title: Text(context.l10n.settingsThumbnailShowVideoDuration),
),
],
);
}
Widget _buildViewerSection(BuildContext context) {
return AvesExpansionTile(
leading: _buildLeading(AIcons.image, stringToColor('Image')),
title: context.l10n.settingsSectionViewer,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
ViewerActionsTile(),
SwitchListTile(
value: settings.showOverlayMinimap,
onChanged: (v) => settings.showOverlayMinimap = v,
title: Text(context.l10n.settingsViewerShowMinimap),
),
SwitchListTile(
value: settings.showOverlayInfo,
onChanged: (v) => settings.showOverlayInfo = v,
title: Text(context.l10n.settingsViewerShowInformation),
subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle),
),
SwitchListTile(
value: settings.showOverlayShootingDetails,
onChanged: settings.showOverlayInfo ? (v) => settings.showOverlayShootingDetails = v : null,
title: Text(context.l10n.settingsViewerShowShootingDetails),
),
ListTile(
title: Text(context.l10n.settingsRasterImageBackground),
trailing: EntryBackgroundSelector(
getter: () => settings.rasterBackground,
setter: (value) => settings.rasterBackground = value,
),
),
ListTile(
title: Text(context.l10n.settingsVectorImageBackground),
trailing: EntryBackgroundSelector(
getter: () => settings.vectorBackground,
setter: (value) => settings.vectorBackground = value,
),
),
],
);
}
Widget _buildVideoSection(BuildContext context) {
final hiddenFilters = settings.hiddenFilters;
final showVideos = !hiddenFilters.contains(MimeFilter.video);
return AvesExpansionTile(
leading: _buildLeading(AIcons.video, stringToColor('Video')),
title: context.l10n.settingsSectionVideo,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
SwitchListTile(
value: showVideos,
onChanged: (v) => context.read<CollectionSource>().changeFilterVisibility(MimeFilter.video, v),
title: Text(context.l10n.settingsVideoShowVideos),
),
SwitchListTile(
value: settings.enableVideoHardwareAcceleration,
onChanged: (v) => settings.enableVideoHardwareAcceleration = v,
title: Text(context.l10n.settingsVideoEnableHardwareAcceleration),
),
SwitchListTile(
value: settings.enableVideoAutoPlay,
onChanged: (v) => settings.enableVideoAutoPlay = v,
title: Text(context.l10n.settingsVideoEnableAutoPlay),
),
ListTile(
title: Text(context.l10n.settingsVideoLoopModeTile),
subtitle: Text(settings.videoLoopMode.getName(context)),
onTap: () async {
final value = await showDialog<VideoLoopMode>(
context: context,
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
initialValue: settings.videoLoopMode,
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.settingsVideoLoopModeTitle,
),
);
if (value != null) {
settings.videoLoopMode = value;
}
},
),
VideoActionsTile(),
],
);
}
Widget _buildPrivacySection(BuildContext context) {
return AvesExpansionTile(
leading: _buildLeading(AIcons.privacy, stringToColor('Privacy')),
title: context.l10n.settingsSectionPrivacy,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
SwitchListTile(
value: settings.isCrashlyticsEnabled,
onChanged: (v) => settings.isCrashlyticsEnabled = v,
title: Text(context.l10n.settingsEnableAnalytics),
),
SwitchListTile(
value: settings.saveSearchHistory,
onChanged: (v) {
settings.saveSearchHistory = v;
if (!v) {
settings.searchHistory = [];
}
},
title: Text(context.l10n.settingsSaveSearchHistory),
),
HiddenFilterTile(),
StorageAccessTile(),
],
);
}
Widget _buildLanguageSection(BuildContext context) {
return AvesExpansionTile(
// use a fixed value instead of the title to identify this expansion tile
// so that the tile state is kept when the language is modified
value: 'language',
leading: _buildLeading(AIcons.language, stringToColor('Language')),
title: context.l10n.settingsSectionLanguage,
expandedNotifier: _expandedNotifier,
showHighlight: false,
children: [
LanguageTile(),
ListTile(
title: Text(context.l10n.settingsCoordinateFormatTile),
subtitle: Text(settings.coordinateFormat.getName(context)),
onTap: () async {
final value = await showDialog<CoordinateFormat>(
context: context,
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
initialValue: settings.coordinateFormat,
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
optionSubtitleBuilder: (value) => value.format(Constants.pointNemo),
title: context.l10n.settingsCoordinateFormatTitle,
),
);
if (value != null) {
settings.coordinateFormat = value;
}
},
),
],
);
}
Widget _buildLeading(IconData icon, Color color) => Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
border: Border.fromBorderSide(BorderSide(
color: color,
width: AvesFilterChip.outlineWidth,
)),
shape: BoxShape.circle,
),
child: DecoratedIcon(
icon,
shadows: Constants.embossShadows,
size: 18,
),
);
}

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/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/quick_actions/editor_page.dart';
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
import 'package:flutter/material.dart';
class VideoActionsTile extends StatelessWidget {

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/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/quick_actions/editor_page.dart';
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
import 'package:flutter/material.dart';
class ViewerActionsTile extends StatelessWidget {

View file

@ -40,7 +40,7 @@ class AssParser {
static const noBreakSpace = '\u00A0';
// Parse text with ASS format tags
// Parse text with ASS style overrides
// cf https://aegi.vmoe.info/docs/3.0/ASS_Tags/
// e.g. `And I'm like, "We can't {\i1}not{\i0} see it."`
// e.g. `{\fad(200,200)\blur3}lorem ipsum"`

View file

@ -1,3 +1,4 @@
import 'package:aves/model/settings/settings.dart';
import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/basic/outlined_text.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
@ -15,16 +16,13 @@ class VideoSubtitles extends StatelessWidget {
final ValueNotifier<ViewState> viewStateNotifier;
final bool debugMode;
static const baseStyle = TextStyle(
color: Colors.white,
fontSize: 20,
shadows: [
Shadow(
color: Colors.black54,
offset: Offset(1, 1),
),
],
);
static const baseOutlineColor = Colors.black;
static const baseShadows = [
Shadow(
color: Colors.black54,
offset: Offset(1, 1),
),
];
const VideoSubtitles({
Key? key,
@ -37,209 +35,223 @@ class VideoSubtitles extends StatelessWidget {
Widget build(BuildContext context) {
final videoDisplaySize = controller.entry.videoDisplaySize(controller.sarNotifier.value);
return IgnorePointer(
child: Selector<MediaQueryData, Orientation>(
selector: (c, mq) => mq.orientation,
builder: (c, orientation, child) {
final bottom = orientation == Orientation.portrait ? .5 : .8;
Alignment toVerticalAlignment(SubtitleStyle extraStyle) {
switch (extraStyle.vAlign) {
case TextAlignVertical.top:
return Alignment(0, -bottom);
case TextAlignVertical.center:
return Alignment.center;
case TextAlignVertical.bottom:
default:
return Alignment(0, bottom);
}
}
child: Consumer<Settings>(
builder: (context, settings, child) {
final baseTextAlign = settings.subtitleTextAlignment;
final baseOutlineWidth = settings.subtitleShowOutline ? 1 : 0;
final baseStyle = TextStyle(
color: settings.subtitleTextColor,
backgroundColor: settings.subtitleBackgroundColor,
fontSize: settings.subtitleFontSize,
shadows: settings.subtitleShowOutline ? baseShadows : null,
);
final viewportSize = context.read<MediaQueryData>().size;
return Selector<MediaQueryData, Orientation>(
selector: (c, mq) => mq.orientation,
builder: (c, orientation, child) {
final bottom = orientation == Orientation.portrait ? .5 : .8;
Alignment toVerticalAlignment(SubtitleStyle extraStyle) {
switch (extraStyle.vAlign) {
case TextAlignVertical.top:
return Alignment(0, -bottom);
case TextAlignVertical.center:
return Alignment.center;
case TextAlignVertical.bottom:
default:
return Alignment(0, bottom);
}
}
return ValueListenableBuilder<ViewState>(
valueListenable: viewStateNotifier,
builder: (context, viewState, child) {
final viewPosition = viewState.position;
final viewScale = viewState.scale ?? 1;
final viewSize = videoDisplaySize * viewScale;
final viewOffset = Offset(
(viewportSize.width - viewSize.width) / 2,
(viewportSize.height - viewSize.height) / 2,
);
final viewportSize = context.read<MediaQueryData>().size;
return StreamBuilder<String?>(
stream: controller.timedTextStream,
builder: (context, snapshot) {
final text = snapshot.data;
if (text == null) return const SizedBox();
return ValueListenableBuilder<ViewState>(
valueListenable: viewStateNotifier,
builder: (context, viewState, child) {
final viewPosition = viewState.position;
final viewScale = viewState.scale ?? 1;
final viewSize = videoDisplaySize * viewScale;
final viewOffset = Offset(
(viewportSize.width - viewSize.width) / 2,
(viewportSize.height - viewSize.height) / 2,
);
if (debugMode) {
return Padding(
padding: const EdgeInsets.only(top: 100.0),
child: Align(
alignment: Alignment.topLeft,
child: OutlinedText(
textSpans: [
TextSpan(
text: text,
style: const TextStyle(fontSize: 14),
)
],
outlineWidth: 1,
outlineColor: Colors.black,
),
),
);
}
return StreamBuilder<String?>(
stream: controller.timedTextStream,
builder: (context, snapshot) {
final text = snapshot.data;
if (text == null) return const SizedBox();
final styledLine = AssParser.parse(text, baseStyle, viewScale);
final position = styledLine.position;
final clip = styledLine.clip;
final styledSpans = styledLine.spans;
final byExtraStyle = groupBy<StyledSubtitleSpan, SubtitleStyle>(styledSpans, (v) => v.extraStyle);
return Stack(
children: byExtraStyle.entries.map((kv) {
final extraStyle = kv.key;
final spans = kv.value.map((v) {
final span = v.textSpan;
final style = span.style;
if (position == null || style == null) return span;
final letterSpacing = style.letterSpacing;
final shadows = style.shadows;
return TextSpan(
text: span.text,
style: style.copyWith(
letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null,
shadows: shadows != null
? shadows
.map((v) => Shadow(
color: v.color,
offset: v.offset * viewScale,
blurRadius: v.blurRadius * viewScale,
))
.toList()
: null,
if (debugMode) {
return Padding(
padding: const EdgeInsets.only(top: 100.0),
child: Align(
alignment: Alignment.topLeft,
child: OutlinedText(
textSpans: [
TextSpan(
text: text,
style: const TextStyle(fontSize: 14),
)
],
outlineWidth: 1,
outlineColor: Colors.black,
),
),
);
}).toList();
final drawingPaths = extraStyle.drawingPaths;
Widget child;
if (drawingPaths != null) {
child = CustomPaint(
painter: SubtitlePathPainter(
paths: drawingPaths,
scale: viewScale,
fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
strokeColor: extraStyle.borderColor,
),
);
} else {
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
child = OutlinedText(
textSpans: spans,
outlineWidth: outlineWidth * (position != null ? viewScale : 1),
outlineColor: extraStyle.borderColor ?? Colors.black,
outlineBlurSigma: extraStyle.edgeBlur ?? 0,
textAlign: extraStyle.hAlign ?? TextAlign.center,
);
}
var transform = Matrix4.identity();
final styledLine = AssParser.parse(text, baseStyle, viewScale);
final position = styledLine.position;
final clip = styledLine.clip;
final styledSpans = styledLine.spans;
final byExtraStyle = groupBy<StyledSubtitleSpan, SubtitleStyle>(styledSpans, (v) => v.extraStyle);
return Stack(
children: byExtraStyle.entries.map((kv) {
final extraStyle = kv.key;
final spans = kv.value.map((v) {
final span = v.textSpan;
final style = span.style;
if (position == null || style == null) return span;
if (position != null) {
final para = RenderParagraph(
TextSpan(children: spans),
textDirection: TextDirection.ltr,
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
)..layout(const BoxConstraints());
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
final letterSpacing = style.letterSpacing;
final shadows = style.shadows;
return TextSpan(
text: span.text,
style: style.copyWith(
letterSpacing: letterSpacing != null ? letterSpacing * viewScale : null,
shadows: shadows != null
? shadows
.map((v) => Shadow(
color: v.color,
offset: v.offset * viewScale,
blurRadius: v.blurRadius * viewScale,
))
.toList()
: null,
),
);
}).toList();
final drawingPaths = extraStyle.drawingPaths;
late double anchorOffsetX, anchorOffsetY;
switch (extraStyle.hAlign) {
case TextAlign.left:
anchorOffsetX = 0;
break;
case TextAlign.right:
anchorOffsetX = -textWidth;
break;
case TextAlign.center:
default:
anchorOffsetX = -textWidth / 2;
break;
}
switch (extraStyle.vAlign) {
case TextAlignVertical.top:
anchorOffsetY = 0;
break;
case TextAlignVertical.center:
anchorOffsetY = -textHeight / 2;
break;
case TextAlignVertical.bottom:
default:
anchorOffsetY = -textHeight;
break;
}
final alignOffset = Offset(anchorOffsetX, anchorOffsetY);
final lineOffset = position * viewScale + viewPosition;
final translateOffset = viewOffset + lineOffset + alignOffset;
transform.translate(translateOffset.dx, translateOffset.dy);
}
Widget child;
if (drawingPaths != null) {
child = CustomPaint(
painter: SubtitlePathPainter(
paths: drawingPaths,
scale: viewScale,
fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
strokeColor: extraStyle.borderColor,
),
);
} else {
final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
child = OutlinedText(
textSpans: spans,
outlineWidth: outlineWidth * (position != null ? viewScale : baseOutlineWidth),
outlineColor: extraStyle.borderColor ?? baseOutlineColor,
outlineBlurSigma: extraStyle.edgeBlur ?? 0,
textAlign: extraStyle.hAlign ?? baseTextAlign,
);
}
if (extraStyle.rotating) {
// for perspective
transform.setEntry(3, 2, 0.001);
final x = -toRadians(extraStyle.rotationX ?? 0);
final y = -toRadians(extraStyle.rotationY ?? 0);
final z = -toRadians(extraStyle.rotationZ ?? 0);
if (x != 0) transform.rotateX(x);
if (y != 0) transform.rotateY(y);
if (z != 0) transform.rotateZ(z);
}
if (extraStyle.scaling) {
final x = extraStyle.scaleX ?? 1;
final y = extraStyle.scaleY ?? 1;
transform.scale(x, y);
}
if (extraStyle.shearing) {
final x = extraStyle.shearX ?? 0;
final y = extraStyle.shearY ?? 0;
transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
}
var transform = Matrix4.identity();
if (!transform.isIdentity()) {
child = Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
}
if (position != null) {
final para = RenderParagraph(
TextSpan(children: spans),
textDirection: TextDirection.ltr,
textScaleFactor: context.read<MediaQueryData>().textScaleFactor,
)..layout(const BoxConstraints());
final textWidth = para.getMaxIntrinsicWidth(double.infinity);
final textHeight = para.getMaxIntrinsicHeight(double.infinity);
if (position == null) {
child = Align(
alignment: toVerticalAlignment(extraStyle),
child: child,
);
}
late double anchorOffsetX, anchorOffsetY;
switch (extraStyle.hAlign ?? baseTextAlign) {
case TextAlign.left:
anchorOffsetX = 0;
break;
case TextAlign.right:
anchorOffsetX = -textWidth;
break;
case TextAlign.center:
case TextAlign.start:
case TextAlign.end:
case TextAlign.justify:
anchorOffsetX = -textWidth / 2;
break;
}
switch (extraStyle.vAlign ?? TextAlignVertical.bottom) {
case TextAlignVertical.top:
anchorOffsetY = 0;
break;
case TextAlignVertical.center:
anchorOffsetY = -textHeight / 2;
break;
case TextAlignVertical.bottom:
anchorOffsetY = -textHeight;
break;
}
final alignOffset = Offset(anchorOffsetX, anchorOffsetY);
final lineOffset = position * viewScale + viewPosition;
final translateOffset = viewOffset + lineOffset + alignOffset;
transform.translate(translateOffset.dx, translateOffset.dy);
}
if (clip != null) {
final clipOffset = viewOffset + viewPosition;
final matrix = Matrix4.identity()
..translate(clipOffset.dx, clipOffset.dy)
..scale(viewScale, viewScale);
final transform = matrix.storage;
child = ClipPath(
clipper: SubtitlePathClipper(
paths: clip.map((v) => v.transform(transform)).toList(),
scale: viewScale,
),
child: child,
);
}
if (extraStyle.rotating) {
// for perspective
transform.setEntry(3, 2, 0.001);
final x = -toRadians(extraStyle.rotationX ?? 0);
final y = -toRadians(extraStyle.rotationY ?? 0);
final z = -toRadians(extraStyle.rotationZ ?? 0);
if (x != 0) transform.rotateX(x);
if (y != 0) transform.rotateY(y);
if (z != 0) transform.rotateZ(z);
}
if (extraStyle.scaling) {
final x = extraStyle.scaleX ?? 1;
final y = extraStyle.scaleY ?? 1;
transform.scale(x, y);
}
if (extraStyle.shearing) {
final x = extraStyle.shearX ?? 0;
final y = extraStyle.shearY ?? 0;
transform.multiply(Matrix4(1, y, 0, 0, x, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1));
}
return child;
}).toList(),
if (!transform.isIdentity()) {
child = Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
}
if (position == null) {
child = Align(
alignment: toVerticalAlignment(extraStyle),
child: child,
);
}
if (clip != null) {
final clipOffset = viewOffset + viewPosition;
final matrix = Matrix4.identity()
..translate(clipOffset.dx, clipOffset.dy)
..scale(viewScale, viewScale);
final transform = matrix.storage;
child = ClipPath(
clipper: SubtitlePathClipper(
paths: clip.map((v) => v.transform(transform)).toList(),
scale: viewScale,
),
child: child,
);
}
return child;
}).toList(),
);
},
);
},
);