diff --git a/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d8d8067 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8367b9a Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b3eca56 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a5d65de Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4cd47b2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c79c58a --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..d4ab59b 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..f74d0ff 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..edb2e51 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..bd3a345 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..c668430 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..e3f9609 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #EE322F + \ No newline at end of file diff --git a/assets/svg/icons/github.svg b/assets/svg/icons/github.svg new file mode 100644 index 0000000..c61cf48 --- /dev/null +++ b/assets/svg/icons/github.svg @@ -0,0 +1,16 @@ + + + + + diff --git a/lib/core/constants/asset_paths.dart b/lib/core/constants/asset_paths.dart index 8260882..84f8e1a 100644 --- a/lib/core/constants/asset_paths.dart +++ b/lib/core/constants/asset_paths.dart @@ -19,4 +19,5 @@ abstract final class AssetPaths { static const iconUtensils = 'assets/svg/icons/utensils.svg'; static const iconFine = 'assets/svg/icons/circle-dollar-sign.svg'; static const iconHistory = 'assets/svg/icons/history.svg'; + static const iconGithub = 'assets/svg/icons/github.svg'; } diff --git a/lib/features/home/home_strings.dart b/lib/features/home/home_strings.dart index b6d565f..82dde68 100644 --- a/lib/features/home/home_strings.dart +++ b/lib/features/home/home_strings.dart @@ -20,4 +20,7 @@ abstract final class HomeStrings { static const restaurantShortcut = 'Central (Meléndez)'; static const comingSoon = 'Muy pronto podrás usar esta función.'; static const allFunctionalitiesTitle = 'Todas las funcionalidades'; + static const repositoryLabel = 'code3743/univalle_app'; + static const repositoryUrl = 'https://github.com/code3743/univalle_app'; + static const cannotOpenRepositoryLink = 'No pudimos abrir el repositorio.'; } diff --git a/lib/features/home/presentation/providers/app_version_provider.dart b/lib/features/home/presentation/providers/app_version_provider.dart new file mode 100644 index 0000000..0d16587 --- /dev/null +++ b/lib/features/home/presentation/providers/app_version_provider.dart @@ -0,0 +1,11 @@ +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_version_provider.g.dart'; + +/// App version shown in the Home footer, e.g. "0.1.0". +@riverpod +Future appVersion(Ref ref) async { + final info = await PackageInfo.fromPlatform(); + return info.version; +} diff --git a/lib/features/home/presentation/views/home_view.dart b/lib/features/home/presentation/views/home_view.dart index b0f4a5d..953b642 100644 --- a/lib/features/home/presentation/views/home_view.dart +++ b/lib/features/home/presentation/views/home_view.dart @@ -17,6 +17,7 @@ import '../../../auth/presentation/viewmodels/auth_view_model.dart'; import '../../../profile/presentation/viewmodels/profile_view_model.dart'; import '../../home_strings.dart'; import '../providers/latest_semester_provider.dart'; +import '../widgets/home_footer.dart'; import '../widgets/home_top_bar.dart'; import '../widgets/program_card.dart'; import '../widgets/quick_access_items.dart'; @@ -145,6 +146,8 @@ class HomeView extends ConsumerWidget { ) .toList(), ), + const SizedBox(height: AppSpacing.lg), + const HomeFooter(), ], ), ), diff --git a/lib/features/home/presentation/widgets/home_footer.dart b/lib/features/home/presentation/widgets/home_footer.dart new file mode 100644 index 0000000..80f4386 --- /dev/null +++ b/lib/features/home/presentation/widgets/home_footer.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../../../core/constants/asset_paths.dart'; +import '../../../../core/extensions/snackbar_extension.dart'; +import '../../home_strings.dart'; +import '../providers/app_version_provider.dart'; + +class HomeFooter extends ConsumerWidget { + const HomeFooter({super.key}); + + Future _openRepository(BuildContext context) async { + final uri = Uri.parse(HomeStrings.repositoryUrl); + final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (!launched && context.mounted) { + context.showSnack(HomeStrings.cannotOpenRepositoryLink); + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colorScheme = Theme.of(context).colorScheme; + final mutedStyle = Theme.of(context).textTheme.bodySmall + ?.copyWith(color: colorScheme.onSurfaceVariant); + final version = ref.watch(appVersionProvider).value; + + return Center( + child: Column( + children: [ + InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () => _openRepository(context), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + AssetPaths.iconGithub, + width: 15, + height: 15, + colorFilter: ColorFilter.mode( + colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 6), + Text(HomeStrings.repositoryLabel, style: mutedStyle), + ], + ), + ), + ), + if (version != null) Text('v$version', style: mutedStyle), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 34e9cec..cf0864c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -265,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -589,6 +597,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" + url: "https://pub.dev" + source: hosted + version: "10.2.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 + url: "https://pub.dev" + source: hosted + version: "4.1.0" path: dependency: transitive description: @@ -963,7 +987,7 @@ packages: source: hosted version: "3.2.6" url_launcher_platform_interface: - dependency: transitive + dependency: "direct dev" description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" @@ -1098,6 +1122,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.26.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + url: "https://pub.dev" + source: hosted + version: "6.4.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7931f9..0f8c5ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: google_fonts: ^8.2.1 html: ^0.15.7 intl: ^0.20.3 + package_info_plus: ^10.2.1 riverpod_annotation: ^4.0.7 shared_preferences: ^2.5.5 url_launcher: ^6.3.1 @@ -33,6 +34,7 @@ dev_dependencies: build_runner: ^2.16.1 flutter_native_splash: ^2.4.8 mocktail: ^1.0.4 + url_launcher_platform_interface: ^2.3.2 flutter: uses-material-design: true diff --git a/test/features/home/presentation/providers/app_version_provider_test.dart b/test/features/home/presentation/providers/app_version_provider_test.dart new file mode 100644 index 0000000..0468a8a --- /dev/null +++ b/test/features/home/presentation/providers/app_version_provider_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:univalle_app/features/home/presentation/providers/app_version_provider.dart'; + +import '../../../../helpers/container.dart'; + +void main() { + setUp(() { + PackageInfo.setMockInitialValues( + appName: 'Univalle App', + packageName: 'co.edu.univalle.univalle_app', + version: '1.2.3', + buildNumber: '7', + buildSignature: '', + ); + }); + + test('reads the version reported by the platform', () async { + final container = createContainer(); + + final version = await container.read(appVersionProvider.future); + + expect(version, '1.2.3'); + }); +} diff --git a/test/features/home/presentation/widgets/home_footer_test.dart b/test/features/home/presentation/widgets/home_footer_test.dart new file mode 100644 index 0000000..713aa27 --- /dev/null +++ b/test/features/home/presentation/widgets/home_footer_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:univalle_app/features/home/home_strings.dart'; +import 'package:univalle_app/features/home/presentation/widgets/home_footer.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import '../../../../helpers/pump_app.dart'; + +class _FakeUrlLauncher extends UrlLauncherPlatform { + _FakeUrlLauncher({this.result = true}); + + final bool result; + String? launchedUrl; + + @override + LinkDelegate? get linkDelegate => null; + + @override + Future launchUrl(String url, LaunchOptions options) async { + launchedUrl = url; + return result; + } +} + +void main() { + setUp(() { + PackageInfo.setMockInitialValues( + appName: 'Univalle App', + packageName: 'co.edu.univalle.univalle_app', + version: '1.2.3', + buildNumber: '7', + buildSignature: '', + ); + }); + + testWidgets('shows the repository label and the app version', (tester) async { + UrlLauncherPlatform.instance = _FakeUrlLauncher(); + + await tester.pumpApp(const HomeFooter()); + await tester.pumpAndSettle(); + + expect(find.text(HomeStrings.repositoryLabel), findsOneWidget); + expect(find.text('v1.2.3'), findsOneWidget); + }); + + testWidgets('opens the repository url when tapped', (tester) async { + final fakeLauncher = _FakeUrlLauncher(); + UrlLauncherPlatform.instance = fakeLauncher; + + await tester.pumpApp(const HomeFooter()); + await tester.pumpAndSettle(); + + await tester.tap(find.text(HomeStrings.repositoryLabel)); + await tester.pumpAndSettle(); + + expect(fakeLauncher.launchedUrl, HomeStrings.repositoryUrl); + }); + + testWidgets('shows a snackbar when the link cannot be opened', ( + tester, + ) async { + UrlLauncherPlatform.instance = _FakeUrlLauncher(result: false); + + await tester.pumpApp(const HomeFooter()); + await tester.pumpAndSettle(); + + await tester.tap(find.text(HomeStrings.repositoryLabel)); + await tester.pumpAndSettle(); + + expect(find.text(HomeStrings.cannotOpenRepositoryLink), findsOneWidget); + }); +}