Merge main
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 262 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 33 KiB |
@ -13,9 +13,9 @@ import 'package:wonders/logic/data/wonders_data/machu_picchu_data.dart';
|
|||||||
import 'package:wonders/logic/data/wonders_data/petra_data.dart';
|
import 'package:wonders/logic/data/wonders_data/petra_data.dart';
|
||||||
import 'package:wonders/logic/data/wonders_data/pyramids_giza_data.dart';
|
import 'package:wonders/logic/data/wonders_data/pyramids_giza_data.dart';
|
||||||
import 'package:wonders/logic/data/wonders_data/taj_mahal_data.dart';
|
import 'package:wonders/logic/data/wonders_data/taj_mahal_data.dart';
|
||||||
|
import 'package:wonders/logic/data/wonders_data/colosseum_data.dart';
|
||||||
|
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/data/wonders_data/colosseum_data.dart';
|
|
||||||
|
|
||||||
class ArtifactDownloadHelper extends StatefulWidget {
|
class ArtifactDownloadHelper extends StatefulWidget {
|
||||||
const ArtifactDownloadHelper({super.key});
|
const ArtifactDownloadHelper({super.key});
|
||||||
@ -82,16 +82,16 @@ class _ArtifactDownloadHelperState extends State<ArtifactDownloadHelper> {
|
|||||||
PyramidsGizaData().searchData +
|
PyramidsGizaData().searchData +
|
||||||
TajMahalData().searchData;
|
TajMahalData().searchData;
|
||||||
|
|
||||||
// for (var a in searchData) {
|
for (var a in searchData) {
|
||||||
// final id = a.id.toString();
|
final id = a.id.toString();
|
||||||
// if (await downloadImageAndJson(id) == false) {
|
if (await downloadImageAndJson(id) == false) {
|
||||||
// missingIds.add(id);
|
missingIds.add(id);
|
||||||
// }
|
}
|
||||||
// final index = searchData.indexOf(a) + 1;
|
final index = searchData.indexOf(a) + 1;
|
||||||
// if (index % 100 == 0) {
|
if (index % 100 == 0) {
|
||||||
// debugPrint('$index/${searchData.length}');
|
debugPrint('$index/${searchData.length}');
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
debugPrint('Download complete :) Missing IDs: $missingIds');
|
debugPrint('Download complete :) Missing IDs: $missingIds');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:desktop_window/desktop_window.dart';
|
import 'package:desktop_window/desktop_window.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/common/platform_info.dart';
|
import 'package:wonders/logic/common/platform_info.dart';
|
||||||
@ -38,11 +39,21 @@ class AppLogic {
|
|||||||
await DesktopWindow.setMinWindowSize($styles.sizes.minAppSize);
|
await DesktopWindow.setMinWindowSize($styles.sizes.minAppSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kIsWeb) {
|
||||||
|
// SB: This is intentionally not a debugPrint, as it's a message for users who open the console on web.
|
||||||
|
print(
|
||||||
|
'''Thanks for checking out Wonderous on the web!
|
||||||
|
If you encounter any issues please report them at https://github.com/gskinnerTeam/flutter-wonderous-app/issues.''',
|
||||||
|
);
|
||||||
|
// Required on web to automatically enable accessibility features
|
||||||
|
WidgetsFlutterBinding.ensureInitialized().ensureSemantics();
|
||||||
|
}
|
||||||
|
|
||||||
// Load any bitmaps the views might need
|
// Load any bitmaps the views might need
|
||||||
await AppBitmaps.init();
|
await AppBitmaps.init();
|
||||||
|
|
||||||
// Set preferred refresh rate to the max possible (the OS may ignore this)
|
// Set preferred refresh rate to the max possible (the OS may ignore this)
|
||||||
if (PlatformInfo.isAndroid) {
|
if (!kIsWeb && PlatformInfo.isAndroid) {
|
||||||
await FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +104,6 @@ class AppLogic {
|
|||||||
|
|
||||||
bool shouldUseNavRail() => _appSize.width > _appSize.height && _appSize.height > 250;
|
bool shouldUseNavRail() => _appSize.width > _appSize.height && _appSize.height > 250;
|
||||||
|
|
||||||
/// Enable landscape, portrait or both. Views can call this method to override the default settings.
|
|
||||||
/// For example, the [FullscreenVideoViewer] always wants to enable both landscape and portrait.
|
|
||||||
/// If a view overrides this, it is responsible for setting it back to [supportedOrientations] when disposed.
|
|
||||||
void _updateSystemOrientation() {
|
void _updateSystemOrientation() {
|
||||||
final axisList = _supportedOrientationsOverride ?? supportedOrientations;
|
final axisList = _supportedOrientationsOverride ?? supportedOrientations;
|
||||||
//debugPrint('updateDeviceOrientation, supportedAxis: $axisList');
|
//debugPrint('updateDeviceOrientation, supportedAxis: $axisList');
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:share_plus/share_plus.dart';
|
|
||||||
import 'package:wonders/common_libs.dart';
|
|
||||||
import 'package:wonders/logic/common/platform_info.dart';
|
|
||||||
import 'package:wonders/ui/common/modals/app_modals.dart';
|
|
||||||
|
|
||||||
class WallPaperLogic {
|
|
||||||
/// Walks user through flow to save a Wonder Poster to their gallery
|
|
||||||
Future<void> save(State state, RenderRepaintBoundary boundary, {required String name}) async {
|
|
||||||
// Time to create an image!
|
|
||||||
Uint8List? pngBytes = await _getPngFromBoundary(boundary);
|
|
||||||
final context = state.context, mounted = state.mounted;
|
|
||||||
if (pngBytes != null && mounted) {
|
|
||||||
bool? result = await showModal(context,
|
|
||||||
child: OkCancelModal(
|
|
||||||
msg: $strings.wallpaperModalSave,
|
|
||||||
));
|
|
||||||
if (result == true && mounted) {
|
|
||||||
showModal(context, child: LoadingModal(msg: $strings.wallpaperModalSaving));
|
|
||||||
if (PlatformInfo.isMobile) {
|
|
||||||
await ImageGallerySaver.saveImage(pngBytes, quality: 95, name: name);
|
|
||||||
} else {
|
|
||||||
await Future.delayed(500.ms);
|
|
||||||
}
|
|
||||||
if (state.mounted) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
showModal(context, child: OkModal(msg: $strings.wallpaperModalSaveComplete));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> share(BuildContext context, RenderRepaintBoundary boundary,
|
|
||||||
{required String name, String wonderName = 'Wonderous'}) async {
|
|
||||||
Uint8List? pngBytes = await _getPngFromBoundary(boundary);
|
|
||||||
if (pngBytes != null) {
|
|
||||||
final directory = (await getApplicationDocumentsDirectory()).path;
|
|
||||||
File imgFile = File('$directory/$name.png');
|
|
||||||
await imgFile.writeAsBytes(pngBytes);
|
|
||||||
Share.shareXFiles([XFile(imgFile.path)],
|
|
||||||
subject: '$wonderName Wallpaper', text: 'Check out this $wonderName wallpaper from the Wonderous app!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Uint8List?> _getPngFromBoundary(RenderRepaintBoundary boundary) async {
|
|
||||||
ui.Image uiImage = await boundary.toImage();
|
|
||||||
ByteData? byteData = await uiImage.toByteData(format: ui.ImageByteFormat.png);
|
|
||||||
if (byteData != null) {
|
|
||||||
return byteData.buffer.asUint8List();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
@ -2,14 +2,14 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/collectibles_logic.dart';
|
|
||||||
import 'package:wonders/logic/locale_logic.dart';
|
|
||||||
import 'package:wonders/logic/artifact_api_logic.dart';
|
import 'package:wonders/logic/artifact_api_logic.dart';
|
||||||
import 'package:wonders/logic/artifact_api_service.dart';
|
import 'package:wonders/logic/artifact_api_service.dart';
|
||||||
|
import 'package:wonders/logic/collectibles_logic.dart';
|
||||||
|
import 'package:wonders/logic/locale_logic.dart';
|
||||||
import 'package:wonders/logic/timeline_logic.dart';
|
import 'package:wonders/logic/timeline_logic.dart';
|
||||||
import 'package:wonders/logic/unsplash_logic.dart';
|
import 'package:wonders/logic/unsplash_logic.dart';
|
||||||
import 'package:wonders/logic/wallpaper_logic.dart';
|
|
||||||
import 'package:wonders/logic/wonders_logic.dart';
|
import 'package:wonders/logic/wonders_logic.dart';
|
||||||
|
import 'package:wonders/ui/common/app_shortcuts.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
@ -37,6 +37,7 @@ class WondersApp extends StatelessWidget with GetItMixin {
|
|||||||
locale: locale == null ? null : Locale(locale),
|
locale: locale == null ? null : Locale(locale),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
routerDelegate: appRouter.routerDelegate,
|
routerDelegate: appRouter.routerDelegate,
|
||||||
|
shortcuts: AppShortcuts.defaults,
|
||||||
theme: ThemeData(fontFamily: $styles.text.body.fontFamily, useMaterial3: true),
|
theme: ThemeData(fontFamily: $styles.text.body.fontFamily, useMaterial3: true),
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
AppLocalizations.delegate,
|
AppLocalizations.delegate,
|
||||||
@ -79,7 +80,6 @@ SettingsLogic get settingsLogic => GetIt.I.get<SettingsLogic>();
|
|||||||
UnsplashLogic get unsplashLogic => GetIt.I.get<UnsplashLogic>();
|
UnsplashLogic get unsplashLogic => GetIt.I.get<UnsplashLogic>();
|
||||||
ArtifactAPILogic get artifactLogic => GetIt.I.get<ArtifactAPILogic>();
|
ArtifactAPILogic get artifactLogic => GetIt.I.get<ArtifactAPILogic>();
|
||||||
CollectiblesLogic get collectiblesLogic => GetIt.I.get<CollectiblesLogic>();
|
CollectiblesLogic get collectiblesLogic => GetIt.I.get<CollectiblesLogic>();
|
||||||
WallPaperLogic get wallpaperLogic => GetIt.I.get<WallPaperLogic>();
|
|
||||||
LocaleLogic get localeLogic => GetIt.I.get<LocaleLogic>();
|
LocaleLogic get localeLogic => GetIt.I.get<LocaleLogic>();
|
||||||
|
|
||||||
/// Global helpers for readability
|
/// Global helpers for readability
|
||||||
|
@ -9,7 +9,6 @@ import 'package:wonders/ui/screens/collection/collection_screen.dart';
|
|||||||
import 'package:wonders/ui/screens/home/wonders_home_screen.dart';
|
import 'package:wonders/ui/screens/home/wonders_home_screen.dart';
|
||||||
import 'package:wonders/ui/screens/intro/intro_screen.dart';
|
import 'package:wonders/ui/screens/intro/intro_screen.dart';
|
||||||
import 'package:wonders/ui/screens/timeline/timeline_screen.dart';
|
import 'package:wonders/ui/screens/timeline/timeline_screen.dart';
|
||||||
import 'package:wonders/ui/screens/wallpaper_photo/wallpaper_photo_screen.dart';
|
|
||||||
import 'package:wonders/ui/screens/wonder_details/wonders_details_screen.dart';
|
import 'package:wonders/ui/screens/wonder_details/wonders_details_screen.dart';
|
||||||
|
|
||||||
/// Shared paths / urls used across the app
|
/// Shared paths / urls used across the app
|
||||||
@ -73,9 +72,6 @@ final appRouter = GoRouter(
|
|||||||
AppRoute('/maps/:type', (s) {
|
AppRoute('/maps/:type', (s) {
|
||||||
return FullscreenMapsViewer(type: _parseWonderType(s.params['type']));
|
return FullscreenMapsViewer(type: _parseWonderType(s.params['type']));
|
||||||
}),
|
}),
|
||||||
AppRoute('/wallpaperPhoto/:type', (s) {
|
|
||||||
return WallpaperPhotoScreen(type: _parseWonderType(s.params['type']));
|
|
||||||
}),
|
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -160,7 +160,7 @@ class _Sizes {
|
|||||||
double get maxContentWidth1 => 800;
|
double get maxContentWidth1 => 800;
|
||||||
double get maxContentWidth2 => 600;
|
double get maxContentWidth2 => 600;
|
||||||
double get maxContentWidth3 => 500;
|
double get maxContentWidth3 => 500;
|
||||||
final Size minAppSize = Size(380, 250);
|
final Size minAppSize = Size(380, 650);
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/common/platform_info.dart';
|
import 'package:wonders/logic/common/platform_info.dart';
|
||||||
|
|
||||||
class AppScrollBehavior extends ScrollBehavior {
|
class AppScrollBehavior extends ScrollBehavior {
|
||||||
@ -15,12 +16,15 @@ class AppScrollBehavior extends ScrollBehavior {
|
|||||||
@override
|
@override
|
||||||
ScrollPhysics getScrollPhysics(BuildContext context) => const BouncingScrollPhysics();
|
ScrollPhysics getScrollPhysics(BuildContext context) => const BouncingScrollPhysics();
|
||||||
|
|
||||||
// TODO: Finalize scrollbar strategy (Do we use them at all? Where specifically?)
|
|
||||||
@override
|
@override
|
||||||
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
|
Widget buildScrollbar(BuildContext context, Widget child, ScrollableDetails details) {
|
||||||
//return child;
|
if (PlatformInfo.isMobile) return child;
|
||||||
return PlatformInfo.isAndroid
|
return RawScrollbar(
|
||||||
? RawScrollbar(controller: details.controller, child: child)
|
controller: details.controller,
|
||||||
: CupertinoScrollbar(controller: details.controller, child: child);
|
thumbVisibility: PlatformInfo.isDesktopOrWeb,
|
||||||
|
thickness: 8,
|
||||||
|
interactive: true,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
lib/ui/common/app_shortcuts.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
|
||||||
|
class AppShortcuts {
|
||||||
|
static final Map<ShortcutActivator, Intent> _defaultWebAndDesktopShortcuts = <ShortcutActivator, Intent>{
|
||||||
|
// Activation
|
||||||
|
if (kIsWeb) ...{
|
||||||
|
// On the web, enter activates buttons, but not other controls.
|
||||||
|
SingleActivator(LogicalKeyboardKey.enter): ButtonActivateIntent(),
|
||||||
|
SingleActivator(LogicalKeyboardKey.numpadEnter): ButtonActivateIntent(),
|
||||||
|
} else ...{
|
||||||
|
SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
|
||||||
|
SingleActivator(LogicalKeyboardKey.numpadEnter): ActivateIntent(),
|
||||||
|
SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
|
||||||
|
SingleActivator(LogicalKeyboardKey.gameButtonA): ActivateIntent(),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dismissal
|
||||||
|
SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
|
||||||
|
|
||||||
|
// Keyboard traversal.
|
||||||
|
SingleActivator(LogicalKeyboardKey.tab): NextFocusIntent(),
|
||||||
|
SingleActivator(LogicalKeyboardKey.tab, shift: true): PreviousFocusIntent(),
|
||||||
|
|
||||||
|
// Scrolling
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowUp): ScrollIntent(direction: AxisDirection.up),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowDown): ScrollIntent(direction: AxisDirection.down),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowLeft): ScrollIntent(direction: AxisDirection.left),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowRight): ScrollIntent(direction: AxisDirection.right),
|
||||||
|
SingleActivator(LogicalKeyboardKey.pageUp):
|
||||||
|
ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||||
|
SingleActivator(LogicalKeyboardKey.pageDown):
|
||||||
|
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||||
|
};
|
||||||
|
|
||||||
|
static Map<ShortcutActivator, Intent>? get defaults {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
// fall back to default shortcuts for ios and android
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return null;
|
||||||
|
// unify shortcuts for desktop/web
|
||||||
|
default:
|
||||||
|
return _defaultWebAndDesktopShortcuts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,8 @@ class AppBtn extends StatelessWidget {
|
|||||||
this.minimumSize,
|
this.minimumSize,
|
||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.border,
|
this.border,
|
||||||
|
this.focusNode,
|
||||||
|
this.onFocusChanged,
|
||||||
}) : _builder = null,
|
}) : _builder = null,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@ -36,6 +38,8 @@ class AppBtn extends StatelessWidget {
|
|||||||
this.minimumSize,
|
this.minimumSize,
|
||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.border,
|
this.border,
|
||||||
|
this.focusNode,
|
||||||
|
this.onFocusChanged,
|
||||||
String? semanticLabel,
|
String? semanticLabel,
|
||||||
String? text,
|
String? text,
|
||||||
AppIcons? icon,
|
AppIcons? icon,
|
||||||
@ -76,6 +80,8 @@ class AppBtn extends StatelessWidget {
|
|||||||
this.isSecondary = false,
|
this.isSecondary = false,
|
||||||
this.circular = false,
|
this.circular = false,
|
||||||
this.minimumSize,
|
this.minimumSize,
|
||||||
|
this.focusNode,
|
||||||
|
this.onFocusChanged,
|
||||||
}) : expand = false,
|
}) : expand = false,
|
||||||
bgColor = Colors.transparent,
|
bgColor = Colors.transparent,
|
||||||
border = null,
|
border = null,
|
||||||
@ -86,6 +92,8 @@ class AppBtn extends StatelessWidget {
|
|||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
late final String semanticLabel;
|
late final String semanticLabel;
|
||||||
final bool enableFeedback;
|
final bool enableFeedback;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final void Function(bool hasFocus)? onFocusChanged;
|
||||||
|
|
||||||
// content:
|
// content:
|
||||||
late final Widget? child;
|
late final Widget? child;
|
||||||
@ -129,15 +137,20 @@ class AppBtn extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Widget button = _CustomFocusBuilder(
|
Widget button = _CustomFocusBuilder(
|
||||||
|
focusNode: focusNode,
|
||||||
|
onFocusChanged: onFocusChanged,
|
||||||
builder: (context, focus) => Stack(
|
builder: (context, focus) => Stack(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
Opacity(
|
||||||
onPressed: onPressed,
|
opacity: onPressed == null ? 0.5 : 1.0,
|
||||||
style: style,
|
child: TextButton(
|
||||||
focusNode: focus,
|
onPressed: onPressed,
|
||||||
child: DefaultTextStyle(
|
style: style,
|
||||||
style: DefaultTextStyle.of(context).style.copyWith(color: textColor),
|
focusNode: focus,
|
||||||
child: content,
|
child: DefaultTextStyle(
|
||||||
|
style: DefaultTextStyle.of(context).style.copyWith(color: textColor),
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (focus.hasFocus)
|
if (focus.hasFocus)
|
||||||
@ -154,7 +167,7 @@ class AppBtn extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// add press effect:
|
// add press effect:
|
||||||
if (pressEffect) button = _ButtonPressEffect(button);
|
if (pressEffect && onPressed != null) button = _ButtonPressEffect(button);
|
||||||
|
|
||||||
// add semantics?
|
// add semantics?
|
||||||
if (semanticLabel.isEmpty) return button;
|
if (semanticLabel.isEmpty) return button;
|
||||||
@ -199,15 +212,28 @@ class _ButtonPressEffectState extends State<_ButtonPressEffect> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CustomFocusBuilder extends StatefulWidget {
|
class _CustomFocusBuilder extends StatefulWidget {
|
||||||
const _CustomFocusBuilder({Key? key, required this.builder}) : super(key: key);
|
const _CustomFocusBuilder({Key? key, required this.builder, this.focusNode, this.onFocusChanged}) : super(key: key);
|
||||||
final Widget Function(BuildContext context, FocusNode focus) builder;
|
final Widget Function(BuildContext context, FocusNode focus) builder;
|
||||||
|
final void Function(bool hasFocus)? onFocusChanged;
|
||||||
|
final FocusNode? focusNode;
|
||||||
@override
|
@override
|
||||||
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
|
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
|
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
|
||||||
late final _focusNode = FocusNode()..addListener(() => setState(() {}));
|
late final FocusNode _focusNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_focusNode = widget.focusNode ?? FocusNode();
|
||||||
|
_focusNode.addListener(_handleFocusChanged);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleFocusChanged() {
|
||||||
|
widget.onFocusChanged?.call(_focusNode.hasFocus);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/ui/common/app_icons.dart';
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart';
|
||||||
|
|
||||||
class CircleBtn extends StatelessWidget {
|
class CircleBtn extends StatelessWidget {
|
||||||
const CircleBtn({
|
const CircleBtn({
|
||||||
@ -14,7 +15,7 @@ class CircleBtn extends StatelessWidget {
|
|||||||
|
|
||||||
static double defaultSize = 48;
|
static double defaultSize = 48;
|
||||||
|
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final BorderSide? border;
|
final BorderSide? border;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -47,6 +48,7 @@ class CircleIconBtn extends StatelessWidget {
|
|||||||
this.color,
|
this.color,
|
||||||
this.size,
|
this.size,
|
||||||
this.iconSize,
|
this.iconSize,
|
||||||
|
this.flipIcon = false,
|
||||||
required this.semanticLabel,
|
required this.semanticLabel,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@ -54,13 +56,14 @@ class CircleIconBtn extends StatelessWidget {
|
|||||||
static double defaultSize = 28;
|
static double defaultSize = 28;
|
||||||
|
|
||||||
final AppIcons icon;
|
final AppIcons icon;
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
final BorderSide? border;
|
final BorderSide? border;
|
||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
final String semanticLabel;
|
final String semanticLabel;
|
||||||
final double? size;
|
final double? size;
|
||||||
final double? iconSize;
|
final double? iconSize;
|
||||||
|
final bool flipIcon;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -72,7 +75,10 @@ class CircleIconBtn extends StatelessWidget {
|
|||||||
size: size,
|
size: size,
|
||||||
bgColor: bgColor ?? defaultColor,
|
bgColor: bgColor ?? defaultColor,
|
||||||
semanticLabel: semanticLabel,
|
semanticLabel: semanticLabel,
|
||||||
child: AppIcon(icon, size: iconSize ?? defaultSize, color: iconColor),
|
child: Transform.scale(
|
||||||
|
scaleX: flipIcon ? -1 : 1,
|
||||||
|
child: AppIcon(icon, size: iconSize ?? defaultSize, color: iconColor),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,8 +109,18 @@ class BackBtn extends StatelessWidget {
|
|||||||
semanticLabel: $strings.circleButtonsSemanticClose,
|
semanticLabel: $strings.circleButtonsSemanticClose,
|
||||||
bgColor: bgColor,
|
bgColor: bgColor,
|
||||||
iconColor: iconColor);
|
iconColor: iconColor);
|
||||||
|
|
||||||
|
bool _handleKeyDown(BuildContext context, KeyDownEvent event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
|
_handleOnPressed(context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
<<<<<<< HEAD
|
||||||
return CircleIconBtn(
|
return CircleIconBtn(
|
||||||
icon: icon,
|
icon: icon,
|
||||||
bgColor: bgColor,
|
bgColor: bgColor,
|
||||||
@ -118,10 +134,29 @@ class BackBtn extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
semanticLabel: semanticLabel ?? $strings.circleButtonsSemanticBack,
|
semanticLabel: semanticLabel ?? $strings.circleButtonsSemanticBack,
|
||||||
|
=======
|
||||||
|
return FullscreenKeyboardListener(
|
||||||
|
onKeyDown: (event) => _handleKeyDown(context, event),
|
||||||
|
child: CircleIconBtn(
|
||||||
|
icon: icon,
|
||||||
|
bgColor: bgColor,
|
||||||
|
color: iconColor,
|
||||||
|
onPressed: () => _handleOnPressed(context),
|
||||||
|
semanticLabel: semanticLabel ?? $strings.circleButtonsSemanticBack,
|
||||||
|
),
|
||||||
|
>>>>>>> main
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget safe() => _SafeAreaWithPadding(child: this);
|
Widget safe() => _SafeAreaWithPadding(child: this);
|
||||||
|
|
||||||
|
void _handleOnPressed(BuildContext context) {
|
||||||
|
if (onPressed != null) {
|
||||||
|
onPressed?.call();
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SafeAreaWithPadding extends StatelessWidget {
|
class _SafeAreaWithPadding extends StatelessWidget {
|
||||||
|
75
lib/ui/common/fullscreen_keyboard_list_scroller.dart
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
import 'package:wonders/logic/common/throttler.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart';
|
||||||
|
|
||||||
|
class FullscreenKeyboardListScroller extends StatelessWidget {
|
||||||
|
FullscreenKeyboardListScroller({super.key, required this.child, required this.scrollController});
|
||||||
|
|
||||||
|
static const int _scrollAmountOnPress = 75;
|
||||||
|
static const int _scrollAmountOnHold = 30;
|
||||||
|
static final Duration _keyPressAnimationDuration = $styles.times.fast * .5;
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final Throttler _throttler = Throttler(32.milliseconds);
|
||||||
|
|
||||||
|
double clampOffset(px) => px.clamp(0, scrollController.position.maxScrollExtent).toDouble();
|
||||||
|
|
||||||
|
void _handleKeyDown(int px) {
|
||||||
|
scrollController.animateTo(
|
||||||
|
clampOffset(scrollController.offset + px),
|
||||||
|
duration: _keyPressAnimationDuration,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleKeyRepeat(int px) {
|
||||||
|
final offset = clampOffset(scrollController.offset + px);
|
||||||
|
_throttler.call(() => scrollController.jumpTo(offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FullscreenKeyboardListener(
|
||||||
|
child: child,
|
||||||
|
onKeyRepeat: (event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||||
|
_handleKeyRepeat(-_scrollAmountOnHold);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
_handleKeyRepeat(_scrollAmountOnHold);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onKeyDown: (event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||||
|
_handleKeyDown(-_scrollAmountOnPress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||||
|
_handleKeyDown(_scrollAmountOnPress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.pageUp) {
|
||||||
|
_handleKeyDown(-_getViewportSize(context));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.pageDown) {
|
||||||
|
_handleKeyDown(_getViewportSize(context));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getViewportSize(BuildContext context) {
|
||||||
|
final rb = context.findRenderObject() as RenderBox?;
|
||||||
|
if (rb != null) {
|
||||||
|
return rb.size.height.round() - 100;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
47
lib/ui/common/fullscreen_keyboard_listener.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
|
||||||
|
class FullscreenKeyboardListener extends StatefulWidget {
|
||||||
|
const FullscreenKeyboardListener({super.key, required this.child, this.onKeyDown, this.onKeyUp, this.onKeyRepeat});
|
||||||
|
final Widget child;
|
||||||
|
final bool Function(KeyDownEvent event)? onKeyDown;
|
||||||
|
final bool Function(KeyUpEvent event)? onKeyUp;
|
||||||
|
final bool Function(KeyRepeatEvent event)? onKeyRepeat;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FullscreenKeyboardListener> createState() => _FullscreenKeyboardListenerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FullscreenKeyboardListenerState extends State<FullscreenKeyboardListener> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ServicesBinding.instance.keyboard.addHandler(_handleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
ServicesBinding.instance.keyboard.removeHandler(_handleKey);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _handleKey(KeyEvent event) {
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
/// Exit early if we are not the current;y focused route (dialog on top?)
|
||||||
|
if (ModalRoute.of(context)?.isCurrent == false) return false;
|
||||||
|
|
||||||
|
if (event is KeyDownEvent && widget.onKeyDown != null) {
|
||||||
|
result = widget.onKeyDown!.call(event);
|
||||||
|
}
|
||||||
|
if (event is KeyUpEvent && widget.onKeyUp != null) {
|
||||||
|
result = widget.onKeyUp!.call(event);
|
||||||
|
}
|
||||||
|
if (event is KeyRepeatEvent && widget.onKeyRepeat != null) {
|
||||||
|
result = widget.onKeyRepeat!.call(event);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.child;
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:wonders/assets.dart';
|
import 'package:wonders/assets.dart';
|
||||||
|
|
||||||
@ -6,5 +5,4 @@ Marker getMapsMarker(LatLng position) => Marker(
|
|||||||
markerId: MarkerId('0'),
|
markerId: MarkerId('0'),
|
||||||
position: position,
|
position: position,
|
||||||
icon: AppBitmaps.mapMarker,
|
icon: AppBitmaps.mapMarker,
|
||||||
anchor: Offset(.5, .7),
|
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_header.dart';
|
import 'package:wonders/ui/common/controls/app_header.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart';
|
||||||
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
||||||
|
|
||||||
class FullscreenUrlImgViewer extends StatefulWidget {
|
class FullscreenUrlImgViewer extends StatefulWidget {
|
||||||
@ -15,7 +19,8 @@ class FullscreenUrlImgViewer extends StatefulWidget {
|
|||||||
|
|
||||||
class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
|
class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
|
||||||
final _isZoomed = ValueNotifier(false);
|
final _isZoomed = ValueNotifier(false);
|
||||||
late final _controller = PageController(initialPage: widget.index);
|
late final _controller = PageController(initialPage: widget.index)..addListener(_handlePageChanged);
|
||||||
|
late final ValueNotifier<int> _currentPage = ValueNotifier(widget.index);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -23,8 +28,35 @@ class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _handleKeyDown(KeyDownEvent event) {
|
||||||
|
int dir = 0;
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
|
||||||
|
dir = -1;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
|
||||||
|
dir = 1;
|
||||||
|
}
|
||||||
|
if (dir != 0) {
|
||||||
|
final focus = FocusManager.instance.primaryFocus;
|
||||||
|
_animateToPage(_currentPage.value + dir);
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
focus?.requestFocus();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePageChanged() => _currentPage.value = _controller.page!.round();
|
||||||
|
|
||||||
void _handleBackPressed() => Navigator.pop(context, _controller.page!.round());
|
void _handleBackPressed() => Navigator.pop(context, _controller.page!.round());
|
||||||
|
|
||||||
|
void _animateToPage(int page) {
|
||||||
|
if (page >= 0 || page < widget.urls.length) {
|
||||||
|
_controller.animateToPage(page, duration: 300.ms, curve: Curves.easeOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget content = AnimatedBuilder(
|
Widget content = AnimatedBuilder(
|
||||||
@ -48,13 +80,46 @@ class _FullscreenUrlImgViewerState extends State<FullscreenUrlImgViewer> {
|
|||||||
child: ExcludeSemantics(child: content),
|
child: ExcludeSemantics(child: content),
|
||||||
);
|
);
|
||||||
|
|
||||||
return Container(
|
return FullscreenKeyboardListener(
|
||||||
color: $styles.colors.black,
|
onKeyDown: _handleKeyDown,
|
||||||
child: Stack(
|
child: Container(
|
||||||
children: [
|
color: $styles.colors.black,
|
||||||
Positioned.fill(child: content),
|
child: Stack(
|
||||||
AppHeader(onBack: _handleBackPressed, isTransparent: true),
|
children: [
|
||||||
],
|
Positioned.fill(child: content),
|
||||||
|
AppHeader(onBack: _handleBackPressed, isTransparent: true),
|
||||||
|
// Show next/previous btns if there are more than one image
|
||||||
|
if (widget.urls.length > 1) ...{
|
||||||
|
BottomCenter(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: $styles.insets.md),
|
||||||
|
child: ValueListenableBuilder(
|
||||||
|
valueListenable: _currentPage,
|
||||||
|
builder: (_, int page, __) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
CircleIconBtn(
|
||||||
|
icon: AppIcons.prev,
|
||||||
|
onPressed: page == 0 ? null : () => _animateToPage(page - 1),
|
||||||
|
semanticLabel: $strings.semanticsNext(''),
|
||||||
|
),
|
||||||
|
Gap($styles.insets.xs),
|
||||||
|
CircleIconBtn(
|
||||||
|
icon: AppIcons.prev,
|
||||||
|
flipIcon: true,
|
||||||
|
onPressed: page == widget.urls.length - 1 ? null : () => _animateToPage(page + 1),
|
||||||
|
semanticLabel: $strings.semanticsNext(''),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
101
lib/ui/common/previous_next_navigation.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:wonders/common_libs.dart';
|
||||||
|
import 'package:wonders/logic/common/platform_info.dart';
|
||||||
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart';
|
||||||
|
|
||||||
|
class PreviousNextNavigation extends StatefulWidget {
|
||||||
|
const PreviousNextNavigation(
|
||||||
|
{super.key,
|
||||||
|
required this.onPreviousPressed,
|
||||||
|
required this.onNextPressed,
|
||||||
|
required this.child,
|
||||||
|
this.maxWidth = 1000,
|
||||||
|
this.nextBtnColor,
|
||||||
|
this.previousBtnColor,
|
||||||
|
this.listenToMouseWheel = true});
|
||||||
|
final VoidCallback? onPreviousPressed;
|
||||||
|
final VoidCallback? onNextPressed;
|
||||||
|
final Color? nextBtnColor;
|
||||||
|
final Color? previousBtnColor;
|
||||||
|
final Widget child;
|
||||||
|
final double? maxWidth;
|
||||||
|
final bool listenToMouseWheel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PreviousNextNavigation> createState() => _PreviousNextNavigationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PreviousNextNavigationState extends State<PreviousNextNavigation> {
|
||||||
|
DateTime _lastMouseScrollTime = DateTime.now();
|
||||||
|
final int _scrollCooldownMs = 300;
|
||||||
|
|
||||||
|
bool _handleKeyDown(KeyDownEvent event) {
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowLeft && widget.onPreviousPressed != null) {
|
||||||
|
widget.onPreviousPressed?.call();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.arrowRight && widget.onNextPressed != null) {
|
||||||
|
widget.onNextPressed?.call();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleMouseScroll(event) {
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
// Cooldown, ignore scroll events that are too close together
|
||||||
|
if (DateTime.now().millisecondsSinceEpoch - _lastMouseScrollTime.millisecondsSinceEpoch < _scrollCooldownMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastMouseScrollTime = DateTime.now();
|
||||||
|
if (event.scrollDelta.dy > 0 && widget.onPreviousPressed != null) {
|
||||||
|
widget.onPreviousPressed!();
|
||||||
|
} else if (event.scrollDelta.dy < 0 && widget.onNextPressed != null) {
|
||||||
|
widget.onNextPressed!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (PlatformInfo.isMobile) return widget.child;
|
||||||
|
return Listener(
|
||||||
|
onPointerSignal: widget.listenToMouseWheel ? _handleMouseScroll : null,
|
||||||
|
child: FullscreenKeyboardListener(
|
||||||
|
onKeyDown: _handleKeyDown,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
widget.child,
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.maxWidth ?? double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: $styles.insets.sm),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
CircleIconBtn(
|
||||||
|
icon: AppIcons.prev,
|
||||||
|
onPressed: widget.onPreviousPressed,
|
||||||
|
semanticLabel: 'Previous',
|
||||||
|
bgColor: widget.previousBtnColor,
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
CircleIconBtn(
|
||||||
|
icon: AppIcons.prev,
|
||||||
|
onPressed: widget.onNextPressed,
|
||||||
|
semanticLabel: 'Next',
|
||||||
|
flipIcon: true,
|
||||||
|
bgColor: widget.nextBtnColor,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,7 @@ class AppHaptics {
|
|||||||
|
|
||||||
static void buttonPress() {
|
static void buttonPress() {
|
||||||
// Android/Fuchsia expect haptics on all button presses, iOS does not.
|
// Android/Fuchsia expect haptics on all button presses, iOS does not.
|
||||||
if (PlatformInfo.isAndroid) {
|
if (!kIsWeb && PlatformInfo.isAndroid) {
|
||||||
lightImpact();
|
lightImpact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class _CollectionListState extends State<_CollectionList> with GetItStateMixin {
|
|||||||
// Maintain scroll position when switching between vertical and horizontal orientation.
|
// Maintain scroll position when switching between vertical and horizontal orientation.
|
||||||
// Multiplies or divides the current scroll position by the ratio of the vertical and horizontal card extents.
|
// Multiplies or divides the current scroll position by the ratio of the vertical and horizontal card extents.
|
||||||
void _maintainScrollPos() {
|
void _maintainScrollPos() {
|
||||||
|
if (scrollController.hasClients == false) return;
|
||||||
const extentFactor = _CollectionList._vtCardExtent / _CollectionList._hzCardExtent;
|
const extentFactor = _CollectionList._vtCardExtent / _CollectionList._hzCardExtent;
|
||||||
final currentPx = scrollController.position.pixels;
|
final currentPx = scrollController.position.pixels;
|
||||||
if (_vtMode.value == true) {
|
if (_vtMode.value == true) {
|
||||||
|
@ -12,8 +12,8 @@ import 'package:wonders/ui/common/app_icons.dart';
|
|||||||
import 'package:wonders/ui/common/blend_mask.dart';
|
import 'package:wonders/ui/common/blend_mask.dart';
|
||||||
import 'package:wonders/ui/common/centered_box.dart';
|
import 'package:wonders/ui/common/centered_box.dart';
|
||||||
import 'package:wonders/ui/common/compass_divider.dart';
|
import 'package:wonders/ui/common/compass_divider.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_header.dart';
|
|
||||||
import 'package:wonders/ui/common/curved_clippers.dart';
|
import 'package:wonders/ui/common/curved_clippers.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_list_scroller.dart';
|
||||||
import 'package:wonders/ui/common/google_maps_marker.dart';
|
import 'package:wonders/ui/common/google_maps_marker.dart';
|
||||||
import 'package:wonders/ui/common/gradient_container.dart';
|
import 'package:wonders/ui/common/gradient_container.dart';
|
||||||
import 'package:wonders/ui/common/hidden_collectible.dart';
|
import 'package:wonders/ui/common/hidden_collectible.dart';
|
||||||
@ -72,7 +72,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
|
|
||||||
/// Attempt to maintain a similar aspect ratio for the image within the app-bar
|
/// Attempt to maintain a similar aspect ratio for the image within the app-bar
|
||||||
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.2;
|
double maxAppBarHeight = min(context.widthPx, $styles.sizes.maxContentWidth1) * 1.2;
|
||||||
bool showBackBtn = appLogic.shouldUseNavRail() == false;
|
final backBtnAlign = appLogic.shouldUseNavRail() ? Alignment.topRight : Alignment.topLeft;
|
||||||
return PopRouterOnOverScroll(
|
return PopRouterOnOverScroll(
|
||||||
controller: _scroller,
|
controller: _scroller,
|
||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
@ -111,54 +111,56 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
padding: widget.contentPadding,
|
padding: widget.contentPadding,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
child: CustomScrollView(
|
child: FullscreenKeyboardListScroller(
|
||||||
primary: false,
|
scrollController: _scroller,
|
||||||
controller: _scroller,
|
child: CustomScrollView(
|
||||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(),
|
controller: _scroller,
|
||||||
key: PageStorageKey('editorial'),
|
scrollBehavior: ScrollConfiguration.of(context).copyWith(),
|
||||||
slivers: [
|
key: PageStorageKey('editorial'),
|
||||||
/// Invisible padding at the top of the list, so the illustration shows through the btm
|
slivers: [
|
||||||
SliverToBoxAdapter(
|
/// Invisible padding at the top of the list, so the illustration shows through the btm
|
||||||
child: SizedBox(height: illustrationHeight),
|
SliverToBoxAdapter(
|
||||||
),
|
child: SizedBox(height: illustrationHeight),
|
||||||
|
|
||||||
/// Text content, animates itself to hide behind the app bar as it scrolls up
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _scrollPos,
|
|
||||||
builder: (_, value, child) {
|
|
||||||
double offsetAmt = max(0, value * .3);
|
|
||||||
double opacity = (1 - offsetAmt / 150).clamp(0, 1);
|
|
||||||
return Transform.translate(
|
|
||||||
offset: Offset(0, offsetAmt),
|
|
||||||
child: Opacity(opacity: opacity, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _TitleText(widget.data, scroller: _scroller),
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
/// Collapsing App bar, pins to the top of the list
|
/// Text content, animates itself to hide behind the app bar as it scrolls up
|
||||||
SliverAppBar(
|
SliverToBoxAdapter(
|
||||||
pinned: true,
|
child: ValueListenableBuilder<double>(
|
||||||
collapsedHeight: minAppBarHeight,
|
valueListenable: _scrollPos,
|
||||||
toolbarHeight: minAppBarHeight,
|
builder: (_, value, child) {
|
||||||
expandedHeight: maxAppBarHeight,
|
double offsetAmt = max(0, value * .3);
|
||||||
backgroundColor: Colors.transparent,
|
double opacity = (1 - offsetAmt / 150).clamp(0, 1);
|
||||||
elevation: 0,
|
return Transform.translate(
|
||||||
leading: SizedBox.shrink(),
|
offset: Offset(0, offsetAmt),
|
||||||
flexibleSpace: SizedBox.expand(
|
child: Opacity(opacity: opacity, child: child),
|
||||||
child: _AppBar(
|
);
|
||||||
widget.data.type,
|
},
|
||||||
scrollPos: _scrollPos,
|
child: _TitleText(widget.data, scroller: _scroller),
|
||||||
sectionIndex: _sectionIndex,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
/// Editorial content (text and images)
|
/// Collapsing App bar, pins to the top of the list
|
||||||
_ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex),
|
SliverAppBar(
|
||||||
],
|
pinned: true,
|
||||||
|
collapsedHeight: minAppBarHeight,
|
||||||
|
toolbarHeight: minAppBarHeight,
|
||||||
|
expandedHeight: maxAppBarHeight,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
leading: SizedBox.shrink(),
|
||||||
|
flexibleSpace: SizedBox.expand(
|
||||||
|
child: _AppBar(
|
||||||
|
widget.data.type,
|
||||||
|
scrollPos: _scrollPos,
|
||||||
|
sectionIndex: _sectionIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Editorial content (text and images)
|
||||||
|
_ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -166,18 +168,23 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
/// Home Btn
|
/// Home Btn
|
||||||
if (showBackBtn) ...[
|
AnimatedBuilder(
|
||||||
AnimatedBuilder(
|
animation: _scroller,
|
||||||
animation: _scroller,
|
builder: (_, child) {
|
||||||
builder: (_, child) {
|
return AnimatedOpacity(
|
||||||
return AnimatedOpacity(
|
opacity: _scrollPos.value > 0 ? 0 : 1,
|
||||||
opacity: _scrollPos.value > 0 ? 0 : 1,
|
duration: $styles.times.med,
|
||||||
duration: $styles.times.med,
|
child: child,
|
||||||
child: child,
|
);
|
||||||
);
|
},
|
||||||
},
|
child: Align(
|
||||||
child: AppHeader(backIcon: AppIcons.north, isTransparent: true))
|
alignment: backBtnAlign,
|
||||||
],
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all($styles.insets.sm),
|
||||||
|
child: BackBtn(icon: AppIcons.north),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -4,6 +4,7 @@ import 'package:wonders/ui/common/app_icons.dart';
|
|||||||
import 'package:wonders/ui/common/controls/app_header.dart';
|
import 'package:wonders/ui/common/controls/app_header.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
||||||
import 'package:wonders/ui/common/gradient_container.dart';
|
import 'package:wonders/ui/common/gradient_container.dart';
|
||||||
|
import 'package:wonders/ui/common/previous_next_navigation.dart';
|
||||||
import 'package:wonders/ui/common/themed_text.dart';
|
import 'package:wonders/ui/common/themed_text.dart';
|
||||||
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
||||||
import 'package:wonders/ui/screens/home_menu/home_menu.dart';
|
import 'package:wonders/ui/screens/home_menu/home_menu.dart';
|
||||||
@ -87,11 +88,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
void _handlePageIndicatorDotPressed(int index) => _setPageIndex(index);
|
void _handlePageIndicatorDotPressed(int index) => _setPageIndex(index);
|
||||||
|
|
||||||
void _setPageIndex(int index) {
|
void _handlePrevNext(int i) => _setPageIndex(_wonderIndex + i, animate: true);
|
||||||
|
|
||||||
|
void _setPageIndex(int index, {bool animate = false}) {
|
||||||
if (index == _wonderIndex) return;
|
if (index == _wonderIndex) return;
|
||||||
// To support infinite scrolling, we can't jump directly to the pressed index. Instead, make it relative to our current position.
|
// To support infinite scrolling, we can't jump directly to the pressed index. Instead, make it relative to our current position.
|
||||||
final pos = ((_pageController.page ?? 0) / _numWonders).floor() * _numWonders;
|
final pos = ((_pageController.page ?? 0) / _numWonders).floor() * _numWonders;
|
||||||
_pageController.jumpToPage(pos + index);
|
final newIndex = pos + index;
|
||||||
|
if (animate == true) {
|
||||||
|
_pageController.animateToPage(newIndex, duration: $styles.times.med, curve: Curves.easeOutCubic);
|
||||||
|
} else {
|
||||||
|
_pageController.jumpToPage(newIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showDetailsPage() async {
|
void _showDetailsPage() async {
|
||||||
@ -125,24 +133,25 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
return _swipeController.wrapGestureDetector(Container(
|
return _swipeController.wrapGestureDetector(Container(
|
||||||
color: $styles.colors.black,
|
color: $styles.colors.black,
|
||||||
child: Stack(
|
child: PreviousNextNavigation(
|
||||||
children: [
|
listenToMouseWheel: false,
|
||||||
Stack(
|
onPreviousPressed: () => _handlePrevNext(-1),
|
||||||
children: [
|
onNextPressed: () => _handlePrevNext(1),
|
||||||
/// Background
|
child: Stack(
|
||||||
..._buildBgAndClouds(),
|
children: [
|
||||||
|
/// Background
|
||||||
|
..._buildBgAndClouds(),
|
||||||
|
|
||||||
/// Wonders Illustrations (main content)
|
/// Wonders Illustrations (main content)
|
||||||
_buildMgPageView(),
|
_buildMgPageView(),
|
||||||
|
|
||||||
/// Foreground illustrations and gradients
|
/// Foreground illustrations and gradients
|
||||||
_buildFgAndGradients(),
|
_buildFgAndGradients(),
|
||||||
|
|
||||||
/// Controls that float on top of the various illustrations
|
/// Controls that float on top of the various illustrations
|
||||||
_buildFloatingUi(),
|
_buildFloatingUi(),
|
||||||
],
|
],
|
||||||
).animate().fadeIn(),
|
).animate().fadeIn(),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
@ -12,12 +11,11 @@ class AboutDialogContent extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
void handleTap(String url) {
|
void handleTap(String url) {
|
||||||
if(PlatformInfo.isDesktopOrWeb){
|
if (PlatformInfo.isDesktopOrWeb) {
|
||||||
launchUrl(Uri.parse(url));
|
launchUrl(Uri.parse(url));
|
||||||
} else {
|
} else {
|
||||||
Navigator.push(context, CupertinoPageRoute(builder: (_) => FullscreenWebView(url)));
|
Navigator.push(context, CupertinoPageRoute(builder: (_) => FullscreenWebView(url)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TextSpan> buildSpan(String text, {Map<String, List<String>>? linkSupplants}) {
|
List<TextSpan> buildSpan(String text, {Map<String, List<String>>? linkSupplants}) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/ui/common/app_icons.dart';
|
import 'package:wonders/logic/common/platform_info.dart';
|
||||||
|
import 'package:wonders/ui/common/app_icons.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
import 'package:wonders/ui/common/controls/app_page_indicator.dart';
|
||||||
import 'package:wonders/ui/common/gradient_container.dart';
|
import 'package:wonders/ui/common/gradient_container.dart';
|
||||||
|
import 'package:wonders/ui/common/previous_next_navigation.dart';
|
||||||
import 'package:wonders/ui/common/static_text_scale.dart';
|
import 'package:wonders/ui/common/static_text_scale.dart';
|
||||||
import 'package:wonders/ui/common/themed_text.dart';
|
import 'package:wonders/ui/common/themed_text.dart';
|
||||||
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
import 'package:wonders/ui/common/utils/app_haptics.dart';
|
||||||
@ -24,13 +25,14 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
static List<_PageData> pageData = [];
|
static List<_PageData> pageData = [];
|
||||||
|
|
||||||
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
|
late final PageController _pageController = PageController()..addListener(_handlePageChanged);
|
||||||
final ValueNotifier<int> _currentPage = ValueNotifier(0);
|
late final ValueNotifier<int> _currentPage = ValueNotifier(0)..addListener(() => setState(() {}));
|
||||||
bool get _isOnLastPage => _currentPage.value.round() == pageData.length - 1;
|
bool get _isOnLastPage => _currentPage.value.round() == pageData.length - 1;
|
||||||
bool get _isOnFirstPage => _currentPage.value.round() == 0;
|
bool get _isOnFirstPage => _currentPage.value.round() == 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pageController.dispose();
|
_pageController.dispose();
|
||||||
|
_currentPage.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,15 +55,13 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
|
|
||||||
void _handleNavTextSemanticTap() => _incrementPage(1);
|
void _handleNavTextSemanticTap() => _incrementPage(1);
|
||||||
|
|
||||||
void _incrementPage(int dir){
|
void _incrementPage(int dir) {
|
||||||
final int current = _pageController.page!.round();
|
final int current = _pageController.page!.round();
|
||||||
if (_isOnLastPage && dir > 0) return;
|
if (_isOnLastPage && dir > 0) return;
|
||||||
if (_isOnFirstPage && dir < 0) return;
|
if (_isOnFirstPage && dir < 0) return;
|
||||||
_pageController.animateToPage(current + dir, duration: 250.ms, curve: Curves.easeIn);
|
_pageController.animateToPage(current + dir, duration: 250.ms, curve: Curves.easeIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleScrollWheel(double delta) => _incrementPage(delta >0? 1 : -1);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Set the page data, as strings may have changed based on locale
|
// Set the page data, as strings may have changed based on locale
|
||||||
@ -78,20 +78,25 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
final List<Widget> pages = pageData.map((e) => _Page(data: e)).toList();
|
final List<Widget> pages = pageData.map((e) => _Page(data: e)).toList();
|
||||||
|
|
||||||
/// Return resulting widget tree
|
/// Return resulting widget tree
|
||||||
return Listener(
|
return DefaultTextColor(
|
||||||
onPointerSignal: (signal){
|
color: $styles.colors.offWhite,
|
||||||
if(signal is PointerScrollEvent){
|
child: ColoredBox(
|
||||||
_handleScrollWheel(signal.scrollDelta.dy);
|
color: $styles.colors.black,
|
||||||
}
|
child: SafeArea(
|
||||||
},
|
child: Animate(
|
||||||
child: DefaultTextColor(
|
delay: 500.ms,
|
||||||
color: $styles.colors.offWhite,
|
effects: const [FadeEffect()],
|
||||||
child: Container(
|
child: PreviousNextNavigation(
|
||||||
color: $styles.colors.black,
|
maxWidth: 600,
|
||||||
child: SafeArea(
|
nextBtnColor: _isOnLastPage ? $styles.colors.accent1 : null,
|
||||||
child: Animate(
|
onPreviousPressed: _isOnFirstPage ? null : () => _incrementPage(-1),
|
||||||
delay: 500.ms,
|
onNextPressed: () {
|
||||||
effects: const [FadeEffect()],
|
if (_isOnLastPage) {
|
||||||
|
_handleIntroCompletePressed();
|
||||||
|
} else {
|
||||||
|
_incrementPage(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// page view with title & description:
|
// page view with title & description:
|
||||||
@ -159,20 +164,22 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
_buildHzGradientOverlay(left: true),
|
_buildHzGradientOverlay(left: true),
|
||||||
_buildHzGradientOverlay(),
|
_buildHzGradientOverlay(),
|
||||||
|
|
||||||
// finish button:
|
|
||||||
Positioned(
|
|
||||||
right: $styles.insets.lg,
|
|
||||||
bottom: $styles.insets.lg,
|
|
||||||
child: _buildFinishBtn(context),
|
|
||||||
),
|
|
||||||
|
|
||||||
// nav help text:
|
// nav help text:
|
||||||
BottomCenter(
|
if (PlatformInfo.isMobile) ...[
|
||||||
child: Padding(
|
// finish button:
|
||||||
padding: EdgeInsets.only(bottom: $styles.insets.lg),
|
Positioned(
|
||||||
child: _buildNavText(context),
|
right: $styles.insets.lg,
|
||||||
|
bottom: $styles.insets.lg,
|
||||||
|
child: _buildFinishBtn(context),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
BottomCenter(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: $styles.insets.lg),
|
||||||
|
child: _buildNavText(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -182,6 +189,24 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildFinishBtn(BuildContext context) {
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: _currentPage,
|
||||||
|
builder: (_, pageIndex, __) {
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: pageIndex == pageData.length - 1 ? 1 : 0,
|
||||||
|
duration: $styles.times.fast,
|
||||||
|
child: CircleIconBtn(
|
||||||
|
icon: AppIcons.next_large,
|
||||||
|
bgColor: $styles.colors.accent1,
|
||||||
|
onPressed: _handleIntroCompletePressed,
|
||||||
|
semanticLabel: $strings.introSemanticEnterApp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildHzGradientOverlay({bool left = false}) {
|
Widget _buildHzGradientOverlay({bool left = false}) {
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment(left ? -1 : 1, 0),
|
alignment: Alignment(left ? -1 : 1, 0),
|
||||||
@ -203,24 +228,6 @@ class _IntroScreenState extends State<IntroScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFinishBtn(BuildContext context) {
|
|
||||||
return ValueListenableBuilder<int>(
|
|
||||||
valueListenable: _currentPage,
|
|
||||||
builder: (_, pageIndex, __) {
|
|
||||||
return AnimatedOpacity(
|
|
||||||
opacity: pageIndex == pageData.length - 1 ? 1 : 0,
|
|
||||||
duration: $styles.times.fast,
|
|
||||||
child: CircleIconBtn(
|
|
||||||
icon: AppIcons.next_large,
|
|
||||||
bgColor: $styles.colors.accent1,
|
|
||||||
onPressed: _handleIntroCompletePressed,
|
|
||||||
semanticLabel: $strings.introSemanticEnterApp,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildNavText(BuildContext context) {
|
Widget _buildNavText(BuildContext context) {
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
valueListenable: _currentPage,
|
valueListenable: _currentPage,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/logic/data/unsplash_photo_data.dart';
|
import 'package:wonders/logic/data/unsplash_photo_data.dart';
|
||||||
import 'package:wonders/ui/common/controls/app_loading_indicator.dart';
|
import 'package:wonders/ui/common/controls/app_loading_indicator.dart';
|
||||||
import 'package:wonders/ui/common/controls/eight_way_swipe_detector.dart';
|
import 'package:wonders/ui/common/controls/eight_way_swipe_detector.dart';
|
||||||
|
import 'package:wonders/ui/common/fullscreen_keyboard_listener.dart';
|
||||||
import 'package:wonders/ui/common/hidden_collectible.dart';
|
import 'package:wonders/ui/common/hidden_collectible.dart';
|
||||||
import 'package:wonders/ui/common/modals/fullscreen_url_img_viewer.dart';
|
import 'package:wonders/ui/common/modals/fullscreen_url_img_viewer.dart';
|
||||||
import 'package:wonders/ui/common/unsplash_photo.dart';
|
import 'package:wonders/ui/common/unsplash_photo.dart';
|
||||||
@ -33,10 +35,16 @@ class _PhotoGalleryState extends State<PhotoGallery> {
|
|||||||
final _photoIds = ValueNotifier<List<String>>([]);
|
final _photoIds = ValueNotifier<List<String>>([]);
|
||||||
int get _imgCount => pow(_gridSize, 2).round();
|
int get _imgCount => pow(_gridSize, 2).round();
|
||||||
|
|
||||||
|
late final List<FocusNode> _focusNodes = List.generate(_imgCount, (index) => FocusNode());
|
||||||
|
|
||||||
|
//TODO: Remove this field (and associated workarounds) once web properly supports ClipPath (https://github.com/flutter/flutter/issues/124675)
|
||||||
|
final bool useClipPathWorkAroundForWeb = kIsWeb;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initPhotoIds();
|
_initPhotoIds();
|
||||||
|
_focusNodes[_index].requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initPhotoIds() async {
|
Future<void> _initPhotoIds() async {
|
||||||
@ -55,6 +63,7 @@ class _PhotoGalleryState extends State<PhotoGallery> {
|
|||||||
if (value < 0 || value >= _imgCount) return;
|
if (value < 0 || value >= _imgCount) return;
|
||||||
_skipNextOffsetTween = skipAnimation;
|
_skipNextOffsetTween = skipAnimation;
|
||||||
setState(() => _index = value);
|
setState(() => _index = value);
|
||||||
|
_focusNodes[value].requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine the required offset to show the current selected index.
|
/// Determine the required offset to show the current selected index.
|
||||||
@ -89,6 +98,37 @@ class _PhotoGalleryState extends State<PhotoGallery> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _handleKeyDown(KeyDownEvent event) {
|
||||||
|
var newIndex = -1;
|
||||||
|
bool handled = false;
|
||||||
|
final key = event.logicalKey;
|
||||||
|
Map<LogicalKeyboardKey, int> keyActions = {
|
||||||
|
LogicalKeyboardKey.arrowUp: -_gridSize,
|
||||||
|
LogicalKeyboardKey.arrowDown: _gridSize,
|
||||||
|
LogicalKeyboardKey.arrowRight: 1,
|
||||||
|
LogicalKeyboardKey.arrowLeft: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
int? action = keyActions[key];
|
||||||
|
if (action != null) {
|
||||||
|
newIndex = _index + action;
|
||||||
|
handled = true;
|
||||||
|
bool isRightSide = _index % _gridSize == _gridSize - 1;
|
||||||
|
if (isRightSide && key == LogicalKeyboardKey.arrowRight) {
|
||||||
|
newIndex = -1;
|
||||||
|
}
|
||||||
|
bool isLeftSide = _index % _gridSize == 0;
|
||||||
|
if (isLeftSide && key == LogicalKeyboardKey.arrowLeft) newIndex = -1;
|
||||||
|
if (newIndex > _gridSize * _gridSize) {
|
||||||
|
newIndex = -1;
|
||||||
|
}
|
||||||
|
if (newIndex >= 0) {
|
||||||
|
_setIndex(newIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts a swipe direction into a new index
|
/// Converts a swipe direction into a new index
|
||||||
void _handleSwipe(Offset dir) {
|
void _handleSwipe(Offset dir) {
|
||||||
// Calculate new index, y swipes move by an entire row, x swipes move one index at a time
|
// Calculate new index, y swipes move by an entire row, x swipes move one index at a time
|
||||||
@ -104,7 +144,8 @@ class _PhotoGalleryState extends State<PhotoGallery> {
|
|||||||
_setIndex(newIndex);
|
_setIndex(newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleImageTapped(int index) async {
|
Future<void> _handleImageTapped(int index, bool isSelected) async {
|
||||||
|
if (_checkCollectibleIndex(index) && isSelected) return;
|
||||||
if (_index == index) {
|
if (_index == index) {
|
||||||
final urls = _photoIds.value.map((e) {
|
final urls = _photoIds.value.map((e) {
|
||||||
return UnsplashPhotoData.getSelfHostedUrl(e, UnsplashPhotoSize.xl);
|
return UnsplashPhotoData.getSelfHostedUrl(e, UnsplashPhotoSize.xl);
|
||||||
@ -122,119 +163,150 @@ class _PhotoGalleryState extends State<PhotoGallery> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleImageFocusChanged(int index, bool isFocused) {
|
||||||
|
if (isFocused) {
|
||||||
|
_setIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _checkCollectibleIndex(int index) {
|
bool _checkCollectibleIndex(int index) {
|
||||||
return index == _getCollectibleIndex() && collectiblesLogic.isLost(widget.wonderType, 1);
|
return index == _getCollectibleIndex() && collectiblesLogic.isLost(widget.wonderType, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ValueListenableBuilder<List<String>>(
|
return FullscreenKeyboardListener(
|
||||||
valueListenable: _photoIds,
|
onKeyDown: _handleKeyDown,
|
||||||
builder: (_, value, __) {
|
child: ValueListenableBuilder<List<String>>(
|
||||||
if (value.isEmpty) {
|
valueListenable: _photoIds,
|
||||||
return Center(child: AppLoadingIndicator());
|
builder: (_, value, __) {
|
||||||
}
|
if (value.isEmpty) {
|
||||||
Size imgSize = context.isLandscape
|
return Center(child: AppLoadingIndicator());
|
||||||
? Size(context.widthPx * .5, context.heightPx * .66)
|
}
|
||||||
: Size(context.widthPx * .66, context.heightPx * .5);
|
Size imgSize = context.isLandscape
|
||||||
imgSize = (widget.imageSize ?? imgSize) * _scale;
|
? Size(context.widthPx * .5, context.heightPx * .66)
|
||||||
// Get transform offset for the current _index
|
: Size(context.widthPx * .66, context.heightPx * .5);
|
||||||
final padding = $styles.insets.md;
|
imgSize = (widget.imageSize ?? imgSize) * _scale;
|
||||||
var gridOffset = _calculateCurrentOffset(padding, imgSize);
|
// Get transform offset for the current _index
|
||||||
gridOffset += Offset(0, -context.mq.padding.top / 2);
|
final padding = $styles.insets.md;
|
||||||
final offsetTweenDuration = _skipNextOffsetTween ? Duration.zero : swipeDuration;
|
var gridOffset = _calculateCurrentOffset(padding, imgSize);
|
||||||
final cutoutTweenDuration = _skipNextOffsetTween ? Duration.zero : swipeDuration * .5;
|
gridOffset += Offset(0, -context.mq.padding.top / 2);
|
||||||
return _AnimatedCutoutOverlay(
|
final offsetTweenDuration = _skipNextOffsetTween ? Duration.zero : swipeDuration;
|
||||||
animationKey: ValueKey(_index),
|
final cutoutTweenDuration = _skipNextOffsetTween ? Duration.zero : swipeDuration * .5;
|
||||||
cutoutSize: imgSize,
|
return _AnimatedCutoutOverlay(
|
||||||
swipeDir: _lastSwipeDir,
|
animationKey: ValueKey(_index),
|
||||||
duration: cutoutTweenDuration,
|
cutoutSize: imgSize,
|
||||||
opacity: _scale == 1 ? .7 : .5,
|
swipeDir: _lastSwipeDir,
|
||||||
child: SafeArea(
|
duration: cutoutTweenDuration,
|
||||||
bottom: false,
|
opacity: _scale == 1 ? .7 : .5,
|
||||||
// Place content in overflow box, to allow it to flow outside the parent
|
enabled: useClipPathWorkAroundForWeb == false,
|
||||||
child: OverflowBox(
|
child: SafeArea(
|
||||||
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
|
bottom: false,
|
||||||
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
|
// Place content in overflow box, to allow it to flow outside the parent
|
||||||
alignment: Alignment.center,
|
child: OverflowBox(
|
||||||
// Detect swipes in order to change index
|
maxWidth: _gridSize * imgSize.width + padding * (_gridSize - 1),
|
||||||
child: EightWaySwipeDetector(
|
maxHeight: _gridSize * imgSize.height + padding * (_gridSize - 1),
|
||||||
onSwipe: _handleSwipe,
|
alignment: Alignment.center,
|
||||||
threshold: 30,
|
// Detect swipes in order to change index
|
||||||
// A tween animation builder moves from image to image based on current offset
|
child: EightWaySwipeDetector(
|
||||||
child: TweenAnimationBuilder<Offset>(
|
onSwipe: _handleSwipe,
|
||||||
tween: Tween(begin: gridOffset, end: gridOffset),
|
threshold: 30,
|
||||||
duration: offsetTweenDuration,
|
// A tween animation builder moves from image to image based on current offset
|
||||||
curve: Curves.easeOut,
|
child: TweenAnimationBuilder<Offset>(
|
||||||
builder: (_, value, child) => Transform.translate(offset: value, child: child),
|
tween: Tween(begin: gridOffset, end: gridOffset),
|
||||||
child: GridView.count(
|
duration: offsetTweenDuration,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
curve: Curves.easeOut,
|
||||||
crossAxisCount: _gridSize,
|
builder: (_, value, child) => Transform.translate(offset: value, child: child),
|
||||||
childAspectRatio: imgSize.aspectRatio,
|
child: FocusTraversalGroup(
|
||||||
mainAxisSpacing: padding,
|
//policy: OrderedTraversalPolicy(),
|
||||||
crossAxisSpacing: padding,
|
child: GridView.count(
|
||||||
children: List.generate(_imgCount, (i) => _buildImage(i, swipeDuration, imgSize)),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisCount: _gridSize,
|
||||||
|
childAspectRatio: imgSize.aspectRatio,
|
||||||
|
mainAxisSpacing: padding,
|
||||||
|
crossAxisSpacing: padding,
|
||||||
|
children: List.generate(_imgCount, (i) => _buildImage(i, swipeDuration, imgSize)),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImage(int index, Duration swipeDuration, Size imgSize) {
|
Widget _buildImage(int index, Duration swipeDuration, Size imgSize) {
|
||||||
/// Bind to collectibles.statesById because we might need to rebuild if a collectible is found.
|
/// Bind to collectibles.statesById because we might need to rebuild if a collectible is found.
|
||||||
return ValueListenableBuilder(
|
return FocusTraversalOrder(
|
||||||
valueListenable: collectiblesLogic.statesById,
|
order: NumericFocusOrder(index.toDouble()),
|
||||||
builder: (_, __, ___) {
|
child: ValueListenableBuilder(
|
||||||
bool selected = index == _index;
|
valueListenable: collectiblesLogic.statesById,
|
||||||
final imgUrl = _photoIds.value[index];
|
builder: (_, __, ___) {
|
||||||
|
bool isSelected = index == _index;
|
||||||
|
final imgUrl = _photoIds.value[index];
|
||||||
|
late String semanticLbl;
|
||||||
|
if (_checkCollectibleIndex(index)) {
|
||||||
|
semanticLbl = $strings.collectibleItemSemanticCollectible;
|
||||||
|
} else {
|
||||||
|
semanticLbl = !isSelected
|
||||||
|
? $strings.photoGallerySemanticFocus(index + 1, _imgCount)
|
||||||
|
: $strings.photoGallerySemanticFullscreen(index + 1, _imgCount);
|
||||||
|
}
|
||||||
|
|
||||||
late String semanticLbl;
|
final photoWidget = TweenAnimationBuilder<double>(
|
||||||
if (_checkCollectibleIndex(index)) {
|
duration: $styles.times.med,
|
||||||
semanticLbl = $strings.collectibleItemSemanticCollectible;
|
curve: Curves.easeOut,
|
||||||
} else {
|
tween: Tween(begin: 1, end: isSelected ? 1.15 : 1),
|
||||||
semanticLbl = !selected
|
builder: (_, value, child) => Transform.scale(scale: value, child: child),
|
||||||
? $strings.photoGallerySemanticFocus(index + 1, _imgCount)
|
child: UnsplashPhoto(
|
||||||
: $strings.photoGallerySemanticFullscreen(index + 1, _imgCount);
|
imgUrl,
|
||||||
}
|
fit: BoxFit.cover,
|
||||||
return MergeSemantics(
|
size: UnsplashPhotoSize.large,
|
||||||
child: Semantics(
|
).animate().fade(),
|
||||||
focused: selected,
|
);
|
||||||
image: !_checkCollectibleIndex(index),
|
|
||||||
liveRegion: selected,
|
return MergeSemantics(
|
||||||
onIncrease: () => _handleImageTapped(_index + 1),
|
child: Semantics(
|
||||||
onDecrease: () => _handleImageTapped(_index - 1),
|
focused: isSelected,
|
||||||
child: AppBtn.basic(
|
image: !_checkCollectibleIndex(index),
|
||||||
semanticLabel: semanticLbl,
|
liveRegion: isSelected,
|
||||||
onPressed: () {
|
onIncrease: () => _handleImageTapped(_index + 1, false),
|
||||||
if (_checkCollectibleIndex(index) && selected) return;
|
onDecrease: () => _handleImageTapped(_index - 1, false),
|
||||||
_handleImageTapped(index);
|
child: AppBtn.basic(
|
||||||
},
|
semanticLabel: semanticLbl,
|
||||||
child: _checkCollectibleIndex(index)
|
focusNode: _focusNodes[index],
|
||||||
? HiddenCollectible(widget.wonderType, index: 1, size: 100)
|
onFocusChanged: (isFocused) => _handleImageFocusChanged(index, isFocused),
|
||||||
: ClipRRect(
|
onPressed: () => _handleImageTapped(index, isSelected),
|
||||||
borderRadius: BorderRadius.circular(8),
|
child: _checkCollectibleIndex(index)
|
||||||
child: SizedBox(
|
? Center(child: HiddenCollectible(widget.wonderType, index: 1, size: 100))
|
||||||
width: imgSize.width,
|
: ClipRRect(
|
||||||
height: imgSize.height,
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: TweenAnimationBuilder<double>(
|
child: SizedBox(
|
||||||
duration: $styles.times.med,
|
width: imgSize.width,
|
||||||
curve: Curves.easeOut,
|
height: imgSize.height,
|
||||||
tween: Tween(begin: 1, end: selected ? 1.15 : 1),
|
child: (useClipPathWorkAroundForWeb == false)
|
||||||
builder: (_, value, child) => Transform.scale(scale: value, child: child),
|
? photoWidget
|
||||||
child: UnsplashPhoto(
|
: Stack(
|
||||||
imgUrl,
|
children: [
|
||||||
fit: BoxFit.cover,
|
photoWidget,
|
||||||
size: UnsplashPhotoSize.large,
|
// Because the web platform doesn't support clipPath, we use a workaround to highlight the selected image
|
||||||
).animate().fade(),
|
Positioned.fill(
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: $styles.times.med,
|
||||||
|
opacity: isSelected ? 0 : .7,
|
||||||
|
child: ColoredBox(color: $styles.colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,23 +4,26 @@ part of '../photo_gallery.dart';
|
|||||||
/// When animationKey changes, the box animates its size, shrinking then returning to its original size.
|
/// When animationKey changes, the box animates its size, shrinking then returning to its original size.
|
||||||
/// Uses[_CutoutClipper] to create the cutout.
|
/// Uses[_CutoutClipper] to create the cutout.
|
||||||
class _AnimatedCutoutOverlay extends StatelessWidget {
|
class _AnimatedCutoutOverlay extends StatelessWidget {
|
||||||
const _AnimatedCutoutOverlay(
|
const _AnimatedCutoutOverlay({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.cutoutSize,
|
required this.cutoutSize,
|
||||||
required this.animationKey,
|
required this.animationKey,
|
||||||
this.duration,
|
this.duration,
|
||||||
required this.swipeDir,
|
required this.swipeDir,
|
||||||
required this.opacity})
|
required this.opacity,
|
||||||
: super(key: key);
|
required this.enabled,
|
||||||
|
}) : super(key: key);
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Size cutoutSize;
|
final Size cutoutSize;
|
||||||
final Key animationKey;
|
final Key animationKey;
|
||||||
final Offset swipeDir;
|
final Offset swipeDir;
|
||||||
final Duration? duration;
|
final Duration? duration;
|
||||||
final double opacity;
|
final double opacity;
|
||||||
|
final bool enabled;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (!enabled) return child;
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
child,
|
child,
|
||||||
|
@ -1,176 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:wonders/common_libs.dart';
|
|
||||||
import 'package:wonders/logic/common/platform_info.dart';
|
|
||||||
import 'package:wonders/logic/data/wonder_data.dart';
|
|
||||||
import 'package:wonders/ui/common/app_icons.dart';
|
|
||||||
import 'package:wonders/ui/common/controls/checkbox.dart';
|
|
||||||
import 'package:wonders/ui/wonder_illustrations/common/animated_clouds.dart';
|
|
||||||
import 'package:wonders/ui/wonder_illustrations/common/wonder_illustration.dart';
|
|
||||||
import 'package:wonders/ui/wonder_illustrations/common/wonder_illustration_config.dart';
|
|
||||||
import 'package:wonders/ui/wonder_illustrations/common/wonder_title_text.dart';
|
|
||||||
|
|
||||||
class WallpaperPhotoScreen extends StatefulWidget {
|
|
||||||
const WallpaperPhotoScreen({Key? key, required this.type}) : super(key: key);
|
|
||||||
final WonderType type;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WallpaperPhotoScreen> createState() => _WallpaperPhotoScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WallpaperPhotoScreenState extends State<WallpaperPhotoScreen> {
|
|
||||||
final GlobalKey _containerKey = GlobalKey();
|
|
||||||
Widget? _illustration;
|
|
||||||
|
|
||||||
bool _showTitleText = true;
|
|
||||||
Timer? _photoRetryTimer;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_photoRetryTimer?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleTakePhoto(BuildContext context, String wonderName) async {
|
|
||||||
final boundary = _containerKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
|
||||||
if (boundary != null) {
|
|
||||||
wallpaperLogic.save(this, boundary, name: '${wonderName}_wallpaper');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleSharePhoto(BuildContext context, String wonderName) async {
|
|
||||||
final boundary = _containerKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
|
||||||
wallpaperLogic.share(context, boundary, name: '${wonderName}_wallpaper', wonderName: wonderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleTextToggle(bool? isActive) {
|
|
||||||
setState(() => _showTitleText = isActive ?? !_showTitleText);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
WonderData wonderData = wondersLogic.getData(widget.type);
|
|
||||||
WonderIllustrationConfig bgConfig = WonderIllustrationConfig.bg(
|
|
||||||
enableAnims: false,
|
|
||||||
enableHero: false,
|
|
||||||
);
|
|
||||||
WonderIllustrationConfig fgConfig = WonderIllustrationConfig(
|
|
||||||
enableAnims: false,
|
|
||||||
enableHero: false,
|
|
||||||
enableBg: false,
|
|
||||||
);
|
|
||||||
Color fgColor = wonderData.type.bgColor; //.withOpacity(.5);
|
|
||||||
|
|
||||||
_illustration = RepaintBoundary(
|
|
||||||
key: _containerKey,
|
|
||||||
child: ClipRect(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// Background - apply additional filter to make moon brighter
|
|
||||||
WonderIllustration(
|
|
||||||
widget.type,
|
|
||||||
config: bgConfig,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Clouds
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: 1,
|
|
||||||
heightFactor: .5,
|
|
||||||
child: AnimatedClouds(
|
|
||||||
wonderType: wonderData.type,
|
|
||||||
opacity: 1,
|
|
||||||
enableAnimations: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Wonder illustration
|
|
||||||
WonderIllustration(
|
|
||||||
widget.type,
|
|
||||||
config: fgConfig,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Foreground gradient
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
fgColor.withOpacity(0),
|
|
||||||
fgColor.withOpacity(fgColor.opacity * .75),
|
|
||||||
],
|
|
||||||
stops: const [0, 1],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Title text
|
|
||||||
if (_showTitleText)
|
|
||||||
BottomCenter(
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(0.0, -$styles.insets.xl * 2),
|
|
||||||
child: WonderTitleText(wonderData, enableShadows: true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Stack(children: [
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(backgroundBlendMode: BlendMode.color, color: Colors.blue),
|
|
||||||
child: _illustration ?? Container(),
|
|
||||||
),
|
|
||||||
TopCenter(
|
|
||||||
child: SafeArea(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all($styles.insets.md),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
|
||||||
child: BackBtn.close(
|
|
||||||
bgColor: $styles.colors.offWhite,
|
|
||||||
iconColor: $styles.colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(child: Container()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0, right: 16.0),
|
|
||||||
child: CircleIconBtn(
|
|
||||||
icon: PlatformInfo.isIOS ? AppIcons.share_ios : AppIcons.share_android,
|
|
||||||
bgColor: $styles.colors.offWhite,
|
|
||||||
color: $styles.colors.black,
|
|
||||||
onPressed: () => _handleSharePhoto(context, wonderData.title),
|
|
||||||
semanticLabel: $strings.wallpaperSemanticSharePhoto,
|
|
||||||
size: 44,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CircleIconBtn(
|
|
||||||
icon: AppIcons.download,
|
|
||||||
onPressed: () => _handleTakePhoto(context, wonderData.title),
|
|
||||||
semanticLabel: $strings.wallpaperSemanticTakePhoto,
|
|
||||||
size: 64,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
BottomCenter(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
SimpleCheckbox(
|
|
||||||
active: _showTitleText, label: $strings.wallpaperCheckboxShowTitle, onToggled: _handleTextToggle),
|
|
||||||
Gap($styles.insets.xl),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,6 @@ import Foundation
|
|||||||
import desktop_window
|
import desktop_window
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
@ -16,7 +15,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
DesktopWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWindowPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
1034
pubspec.lock
@ -23,13 +23,14 @@ dependencies:
|
|||||||
flutter_animate: ^1.0.0
|
flutter_animate: ^1.0.0
|
||||||
flutter_circular_text: ^0.3.1
|
flutter_circular_text: ^0.3.1
|
||||||
flutter_displaymode: ^0.6.0
|
flutter_displaymode: ^0.6.0
|
||||||
|
flutter_native_splash: ^2.3.3
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
flutter_svg: ^2.0.1
|
flutter_svg: ^2.0.1
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
get_it: ^7.2.0
|
get_it: ^7.2.0
|
||||||
get_it_mixin: ^4.2.2
|
get_it_mixin: ^4.2.2
|
||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.3
|
||||||
google_maps_flutter_web: ^0.5.4+2
|
google_maps_flutter_web: ^0.5.4+3
|
||||||
go_router: ^6.5.5
|
go_router: ^6.5.5
|
||||||
home_widget: ^0.3.0
|
home_widget: ^0.3.0
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
@ -44,7 +45,6 @@ dependencies:
|
|||||||
pointer_interceptor: ^0.9.3+6
|
pointer_interceptor: ^0.9.3+6
|
||||||
provider: ^6.0.5
|
provider: ^6.0.5
|
||||||
rnd: ^0.2.0
|
rnd: ^0.2.0
|
||||||
share_plus: ^8.0.0
|
|
||||||
shared_preferences: ^2.0.17
|
shared_preferences: ^2.0.17
|
||||||
sized_context: ^1.0.0+1
|
sized_context: ^1.0.0+1
|
||||||
smooth_page_indicator: ^1.0.1
|
smooth_page_indicator: ^1.0.1
|
||||||
@ -56,7 +56,6 @@ dependencies:
|
|||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
icons_launcher: ^2.1.3
|
icons_launcher: ^2.1.3
|
||||||
flutter_lints: ^2.0.2
|
flutter_lints: ^2.0.2
|
||||||
flutter_native_splash: ^2.3.3
|
|
||||||
dependency_validator: ^3.2.2
|
dependency_validator: ^3.2.2
|
||||||
|
|
||||||
icons_launcher:
|
icons_launcher:
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<!--
|
<!-- Google tag (gtag.js) -->
|
||||||
If you are serving your web app in a path other than the root, change the
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-24FD0ZMQPW"></script>
|
||||||
href value below to reflect the base path you are serving from.
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
gtag('config', 'G-24FD0ZMQPW');
|
||||||
it to work correctly.
|
</script>
|
||||||
|
|
||||||
For more details:
|
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
|
||||||
the `--base-href` argument provided to `flutter build`.
|
|
||||||
-->
|
|
||||||
<base href="/web/">
|
<base href="/web/">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
@ -7,14 +7,11 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <desktop_window/desktop_window_plugin.h>
|
#include <desktop_window/desktop_window_plugin.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
DesktopWindowPluginRegisterWithRegistrar(
|
DesktopWindowPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
|
registry->GetRegistrarForPlugin("DesktopWindowPlugin"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_window
|
desktop_window
|
||||||
share_plus
|
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|