Merge branch 'main' into feature/routing_fixes

This commit is contained in:
Shawn 2024-01-22 15:36:08 -07:00
commit b7b9141682
8 changed files with 85 additions and 57 deletions

View File

@ -47,17 +47,9 @@ android {
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.debug
} }
} }

View File

@ -1,16 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'package:home_widget/home_widget.dart'; import 'package:http/http.dart' as http;
import 'package:wonders/common_libs.dart'; import 'package:wonders/common_libs.dart';
import 'package:wonders/logic/common/save_load_mixin.dart'; import 'package:wonders/logic/common/save_load_mixin.dart';
import 'package:wonders/logic/data/collectible_data.dart'; import 'package:wonders/logic/data/collectible_data.dart';
import 'package:http/http.dart' as http; import 'package:wonders/logic/native_widget_service.dart';
class CollectiblesLogic with ThrottledSaveLoadMixin { class CollectiblesLogic with ThrottledSaveLoadMixin {
@override @override
String get fileName => 'collectibles.dat'; String get fileName => 'collectibles.dat';
static const _appGroupId = 'group.com.gskinner.flutter.wonders.widget';
static const _appName = 'WonderousWidget';
/// Holds all collectibles that the views should care about /// Holds all collectibles that the views should care about
final List<CollectibleData> all = collectiblesData; final List<CollectibleData> all = collectiblesData;
@ -26,9 +24,9 @@ class CollectiblesLogic with ThrottledSaveLoadMixin {
int get exploredCount => _exploredCount; int get exploredCount => _exploredCount;
void init() { late final _nativeWidget = GetIt.I<NativeWidgetService>();
HomeWidget.setAppGroupId(_appGroupId);
} void init() => _nativeWidget.init();
CollectibleData? fromId(String? id) => id == null ? null : all.firstWhereOrNull((o) => o.id == id); CollectibleData? fromId(String? id) => id == null ? null : all.firstWhereOrNull((o) => o.id == id);
@ -42,7 +40,7 @@ class CollectiblesLogic with ThrottledSaveLoadMixin {
statesById.value = states; statesById.value = states;
if (state == CollectibleState.discovered) { if (state == CollectibleState.discovered) {
final data = fromId(id)!; final data = fromId(id)!;
_updateHomeWidgetTextData( _updateNativeWidgetTextData(
title: data.title, title: data.title,
id: data.id, id: data.id,
imageUrl: data.imageUrlSmall, imageUrl: data.imageUrlSmall,
@ -58,9 +56,10 @@ class CollectiblesLogic with ThrottledSaveLoadMixin {
if (state == CollectibleState.explored) _exploredCount++; if (state == CollectibleState.explored) _exploredCount++;
}); });
final foundCount = discoveredCount + exploredCount; final foundCount = discoveredCount + exploredCount;
HomeWidget.saveWidgetData<int>('discoveredCount', foundCount).then((value) { _nativeWidget.save<int>('discoveredCount', foundCount).then((value) {
HomeWidget.updateWidget(iOSName: _appName); _nativeWidget.markDirty();
}); });
debugPrint('setting discoveredCount for home widget $foundCount'); debugPrint('setting discoveredCount for home widget $foundCount');
} }
@ -92,22 +91,24 @@ class CollectiblesLogic with ThrottledSaveLoadMixin {
for (int i = 0; i < all.length; i++) { for (int i = 0; i < all.length; i++) {
states[all[i].id] = CollectibleState.lost; states[all[i].id] = CollectibleState.lost;
} }
_updateHomeWidgetTextData(); // clear home widget data if (_nativeWidget.isSupported) {
_updateNativeWidgetTextData(); // clear home widget data
}
statesById.value = states; statesById.value = states;
debugPrint('collection reset'); debugPrint('collection reset');
scheduleSave(); scheduleSave();
} }
Future<void> _updateHomeWidgetTextData({String title = '', String id = '', String imageUrl = ''}) async { Future<void> _updateNativeWidgetTextData({String title = '', String id = '', String imageUrl = ''}) async {
// Save title // Save title
await HomeWidget.saveWidgetData<String>('lastDiscoveredTitle', title); await _nativeWidget.save<String>('lastDiscoveredTitle', title);
// Subtitle // Subtitle
String subTitle = ''; String subTitle = '';
if (id.isNotEmpty) { if (id.isNotEmpty) {
final artifactData = await artifactLogic.getArtifactByID(id); final artifactData = await artifactLogic.getArtifactByID(id);
subTitle = artifactData?.date ?? ''; subTitle = artifactData?.date ?? '';
} }
await HomeWidget.saveWidgetData<String>('lastDiscoveredSubTitle', subTitle); await _nativeWidget.save<String>('lastDiscoveredSubTitle', subTitle);
// Image, // Image,
// Download, convert to base64 string and write to shared widget data // Download, convert to base64 string and write to shared widget data
String imageBase64 = ''; String imageBase64 = '';
@ -116,8 +117,8 @@ class CollectiblesLogic with ThrottledSaveLoadMixin {
imageBase64 = base64Encode(bytes); imageBase64 = base64Encode(bytes);
debugPrint('Saving base64 bytes: $imageBase64'); debugPrint('Saving base64 bytes: $imageBase64');
} }
await HomeWidget.saveWidgetData<String>('lastDiscoveredImageData', imageBase64); await _nativeWidget.save<String>('lastDiscoveredImageData', imageBase64);
await HomeWidget.updateWidget(iOSName: _appName); await _nativeWidget.markDirty();
} }
@override @override

View File

@ -0,0 +1,28 @@
import 'package:home_widget/home_widget.dart';
import 'package:wonders/logic/common/platform_info.dart';
/// Small facade for the HomeWidget package
class NativeWidgetService {
static const _iosAppGroupId = 'group.com.gskinner.flutter.wonders.widget';
static const _iosAppName = 'WonderousWidget';
final bool isSupported = PlatformInfo.isIOS;
Future<void> init() async {
if (!isSupported) return;
await HomeWidget.setAppGroupId(_iosAppGroupId);
}
Future<bool?> save<T>(String s, T value, {void Function(bool?)? onSaveComplete}) async {
if (!isSupported) return false;
return await HomeWidget.saveWidgetData<T>(s, value).then((value) {
onSaveComplete?.call(value);
return null;
});
}
Future<bool?> markDirty() async {
if (!isSupported) return false;
return await HomeWidget.updateWidget(iOSName: _iosAppName);
}
}

View File

@ -5,6 +5,7 @@ import 'package:wonders/common_libs.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/collectibles_logic.dart';
import 'package:wonders/logic/native_widget_service.dart';
import 'package:wonders/logic/locale_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';
@ -71,6 +72,8 @@ void registerSingletons() {
GetIt.I.registerLazySingleton<CollectiblesLogic>(() => CollectiblesLogic()); GetIt.I.registerLazySingleton<CollectiblesLogic>(() => CollectiblesLogic());
// Localizations // Localizations
GetIt.I.registerLazySingleton<LocaleLogic>(() => LocaleLogic()); GetIt.I.registerLazySingleton<LocaleLogic>(() => LocaleLogic());
// Home Widget Service
GetIt.I.registerLazySingleton<NativeWidgetService>(() => NativeWidgetService());
} }
/// Add syntax sugar for quickly accessing the main "logic" controllers in the app /// Add syntax sugar for quickly accessing the main "logic" controllers in the app

View File

@ -6,7 +6,7 @@ import 'package:wonders/ui/common/utils/app_haptics.dart';
import 'package:wonders/ui/screens/collectible_found/collectible_found_screen.dart'; import 'package:wonders/ui/screens/collectible_found/collectible_found_screen.dart';
class CollectibleItem extends StatelessWidget with GetItMixin { class CollectibleItem extends StatelessWidget with GetItMixin {
CollectibleItem(this.collectible, {this.size = 64.0, Key? key}) : super(key: key) { CollectibleItem(this.collectible, {this.size = 64.0, Key? key, this.focus}) : super(key: key) {
// pre-fetch the image, so it's ready if we show the collectible found screen. // pre-fetch the image, so it's ready if we show the collectible found screen.
_imageProvider = NetworkImage(collectible.imageUrl); _imageProvider = NetworkImage(collectible.imageUrl);
_imageProvider.resolve(ImageConfiguration()).addListener(ImageStreamListener((_, __) {})); _imageProvider.resolve(ImageConfiguration()).addListener(ImageStreamListener((_, __) {}));
@ -15,6 +15,7 @@ class CollectibleItem extends StatelessWidget with GetItMixin {
final CollectibleData collectible; final CollectibleData collectible;
final double size; final double size;
late final ImageProvider _imageProvider; late final ImageProvider _imageProvider;
final FocusNode? focus;
void _handleTap(BuildContext context) async { void _handleTap(BuildContext context) async {
final screen = CollectibleFoundScreen(collectible: collectible, imageProvider: _imageProvider); final screen = CollectibleFoundScreen(collectible: collectible, imageProvider: _imageProvider);
@ -39,6 +40,7 @@ class CollectibleItem extends StatelessWidget with GetItMixin {
// Note: In order for the collapse animation to run properly, we must return a non-zero height or width. // Note: In order for the collapse animation to run properly, we must return a non-zero height or width.
closedBuilder: (_) => SizedBox(width: 1, height: 0), closedBuilder: (_) => SizedBox(width: 1, height: 0),
openBuilder: (_) => AppBtn.basic( openBuilder: (_) => AppBtn.basic(
focusNode: focus,
semanticLabel: $strings.collectibleItemSemanticCollectible, semanticLabel: $strings.collectibleItemSemanticCollectible,
onPressed: () => _handleTap(context), onPressed: () => _handleTap(context),
enableFeedback: false, enableFeedback: false,

View File

@ -232,8 +232,10 @@ class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
void _handleFocusChanged() { void _handleFocusChanged() {
widget.onFocusChanged?.call(_focusNode.hasFocus); widget.onFocusChanged?.call(_focusNode.hasFocus);
if (mounted) {
setState(() {}); setState(() {});
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -5,13 +5,15 @@ import 'package:wonders/ui/common/collectible_item.dart';
/// The item is looked up via index, and expects that 3 items always exist for each wonder. /// The item is looked up via index, and expects that 3 items always exist for each wonder.
/// If `wonders` is empty, then the collectible is always shown. /// If `wonders` is empty, then the collectible is always shown.
class HiddenCollectible extends StatelessWidget with GetItMixin { class HiddenCollectible extends StatelessWidget with GetItMixin {
HiddenCollectible(this.currentWonder, {Key? key, required this.index, this.matches = const [], this.size = 64}) HiddenCollectible(this.currentWonder,
{Key? key, required this.index, this.matches = const [], this.size = 64, this.focus})
: assert(index <= 2, 'index should not exceed 2'), : assert(index <= 2, 'index should not exceed 2'),
super(key: key); super(key: key);
final int index; final int index;
final double size; final double size;
final List<WonderType> matches; final List<WonderType> matches;
final WonderType currentWonder; final WonderType currentWonder;
final FocusNode? focus;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final data = collectiblesLogic.forWonder(currentWonder); final data = collectiblesLogic.forWonder(currentWonder);
@ -19,6 +21,6 @@ class HiddenCollectible extends StatelessWidget with GetItMixin {
if (matches.isNotEmpty && matches.contains(currentWonder) == false) { if (matches.isNotEmpty && matches.contains(currentWonder) == false) {
return SizedBox.shrink(); return SizedBox.shrink();
} }
return CollectibleItem(data[index], size: size); return CollectibleItem(data[index], size: size, focus: focus);
} }
} }

View File

@ -99,8 +99,6 @@ class _PhotoGalleryState extends State<PhotoGallery> {
} }
bool _handleKeyDown(KeyDownEvent event) { bool _handleKeyDown(KeyDownEvent event) {
var newIndex = -1;
bool handled = false;
final key = event.logicalKey; final key = event.logicalKey;
Map<LogicalKeyboardKey, int> keyActions = { Map<LogicalKeyboardKey, int> keyActions = {
LogicalKeyboardKey.arrowUp: -_gridSize, LogicalKeyboardKey.arrowUp: -_gridSize,
@ -109,24 +107,22 @@ class _PhotoGalleryState extends State<PhotoGallery> {
LogicalKeyboardKey.arrowLeft: -1, LogicalKeyboardKey.arrowLeft: -1,
}; };
int? action = keyActions[key]; // Apply key action, exit early if no action is defined
if (action != null) { int? actionValue = keyActions[key];
newIndex = _index + action; if (actionValue == null) return false;
handled = true; int newIndex = _index + actionValue;
// Block actions along edges of the grid
bool isRightSide = _index % _gridSize == _gridSize - 1; bool isRightSide = _index % _gridSize == _gridSize - 1;
if (isRightSide && key == LogicalKeyboardKey.arrowRight) {
newIndex = -1;
}
bool isLeftSide = _index % _gridSize == 0; bool isLeftSide = _index % _gridSize == 0;
if (isLeftSide && key == LogicalKeyboardKey.arrowLeft) newIndex = -1; bool outOfBounds = newIndex < 0 || newIndex >= _imgCount;
if (newIndex > _gridSize * _gridSize) { if ((isRightSide && key == LogicalKeyboardKey.arrowRight) ||
newIndex = -1; (isLeftSide && key == LogicalKeyboardKey.arrowLeft) ||
outOfBounds) {
return false;
} }
if (newIndex >= 0) {
_setIndex(newIndex); _setIndex(newIndex);
} return true;
}
return handled;
} }
/// Converts a swipe direction into a new index /// Converts a swipe direction into a new index
@ -274,14 +270,16 @@ class _PhotoGalleryState extends State<PhotoGallery> {
liveRegion: isSelected, liveRegion: isSelected,
onIncrease: () => _handleImageTapped(_index + 1, false), onIncrease: () => _handleImageTapped(_index + 1, false),
onDecrease: () => _handleImageTapped(_index - 1, false), onDecrease: () => _handleImageTapped(_index - 1, false),
child: AppBtn.basic( child: _checkCollectibleIndex(index)
? Center(
child: HiddenCollectible(widget.wonderType, index: 1, size: 100, focus: _focusNodes[index]),
)
: AppBtn.basic(
semanticLabel: semanticLbl, semanticLabel: semanticLbl,
focusNode: _focusNodes[index], focusNode: _focusNodes[index],
onFocusChanged: (isFocused) => _handleImageFocusChanged(index, isFocused), onFocusChanged: (isFocused) => _handleImageFocusChanged(index, isFocused),
onPressed: () => _handleImageTapped(index, isSelected), onPressed: () => _handleImageTapped(index, isSelected),
child: _checkCollectibleIndex(index) child: ClipRRect(
? Center(child: HiddenCollectible(widget.wonderType, index: 1, size: 100))
: ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: SizedBox( child: SizedBox(
width: imgSize.width, width: imgSize.width,