diff --git a/lib/constants.dart b/lib/constants.dart index ef75c86..5848aec 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -5,6 +5,6 @@ const String supabasePublicKey = String.fromEnvironment('SUPABASE_PUBLIC_KEY'); const String apiHost = String.fromEnvironment('API_HOST', defaultValue: 'http://localhost:8088/lending'); -const String apiKey = String.fromEnvironment('API_KEY', defaultValue: ''); +const String apiKey = String.fromEnvironment('API_KEY'); const String appUrl = String.fromEnvironment('APP_URL'); diff --git a/lib/main.dart b/lib/main.dart index 5a33857..cf85758 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart' as riverpod; import 'package:librarian_app/constants.dart'; +import 'package:librarian_app/src/core/library.dart'; import 'package:librarian_app/src/features/splash/pages/splash_page.dart'; +import 'package:librarian_app/src/services/image_service.dart'; import 'package:librarian_app/src/theme/indigo_theme.dart'; import 'package:supabase_flutter/supabase_flutter.dart' as supabase; @@ -13,6 +15,10 @@ Future main() async { anonKey: supabasePublicKey, ); + if (supabaseUrl.isNotEmpty) { + Library.logoUrl = ImageService().getPublicUrl('library', 'settings/logo'); + } + runApp(const riverpod.ProviderScope( child: LibrarianApp(), )); diff --git a/lib/src/core/library.dart b/lib/src/core/library.dart new file mode 100644 index 0000000..805b389 --- /dev/null +++ b/lib/src/core/library.dart @@ -0,0 +1,3 @@ +class Library { + static String? logoUrl; +} diff --git a/lib/src/features/authentication/pages/signin_page.dart b/lib/src/features/authentication/pages/signin_page.dart index 208e37d..6e1f064 100644 --- a/lib/src/features/authentication/pages/signin_page.dart +++ b/lib/src/features/authentication/pages/signin_page.dart @@ -1,10 +1,14 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/src/core/library.dart'; import 'package:librarian_app/src/features/authentication/providers/signin_error_provider.dart'; import 'package:librarian_app/src/features/authentication/widgets/discord_button.dart'; import 'package:librarian_app/src/features/authentication/providers/auth_service_provider.dart'; import 'package:librarian_app/src/features/dashboard/pages/dashboard_page.dart'; +import 'package:librarian_app/src/widgets/fade_page_route.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class SignInPage extends ConsumerWidget { @@ -14,7 +18,7 @@ class SignInPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { void onSignedIn() { Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const DashboardPage()), + createFadePageRoute(child: const DashboardPage()), (route) => false, ); } @@ -36,37 +40,69 @@ class SignInPage extends ConsumerWidget { } } - return Scaffold( - body: Padding( + final screenSize = MediaQuery.of(context).size; + final cardHeight = min(240, screenSize.height); + final cardWidth = min(cardHeight, screenSize.width); + + final card = Card( + child: Padding( padding: const EdgeInsets.all(16), - child: Center( + child: SizedBox( + height: cardHeight, + width: cardWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ - Image.asset( - "pvd_things.png", - isAntiAlias: true, - width: 160, - ), - const SizedBox(height: 32), + const Spacer(), + _LogoImage(), + const Spacer(), DiscordSigninButton(onPressed: signIn), if (ref.watch(signinErrorProvider) != null) ...[ const SizedBox(height: 16), Text(ref.read(signinErrorProvider)!) ], - const SizedBox(height: 32), - const Card( - child: Padding( - padding: EdgeInsets.all(16), - child: Text( - 'Only authorized users can sign in.\nPlease ask the PVD Things Digital Team for volunteer access.'), - ), - ), + const Spacer(), ], ), ), ), ); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(16), + child: Center(child: card), + ), + ); + } +} + +class _LogoImage extends StatelessWidget { + @override + Widget build(BuildContext context) { + if (Library.logoUrl != null) { + return Image.network( + Library.logoUrl!, + loadingBuilder: (context, child, progress) { + return Center(child: child); + }, + isAntiAlias: true, + height: 120, + ); + } + + if (kDebugMode) { + return Image.asset( + 'pvd_things.png', + isAntiAlias: true, + height: 120, + ); + } + + return const Icon( + Icons.local_library_outlined, + size: 120, + ); } } diff --git a/lib/src/features/authentication/widgets/discord_button.dart b/lib/src/features/authentication/widgets/discord_button.dart index 2477c46..e4a70cb 100644 --- a/lib/src/features/authentication/widgets/discord_button.dart +++ b/lib/src/features/authentication/widgets/discord_button.dart @@ -9,11 +9,12 @@ class DiscordSigninButton extends StatelessWidget { Widget build(BuildContext context) { return ElevatedButton.icon( icon: const Icon(Icons.discord_rounded), - label: const Text('Sign in with Discord'), + label: const Text('Sign in'), onPressed: onPressed, style: ElevatedButton.styleFrom( - textStyle: const TextStyle(fontSize: 20), + backgroundColor: Theme.of(context).primaryColor, padding: const EdgeInsets.all(16), + textStyle: const TextStyle(fontSize: 20), ), ); } diff --git a/lib/src/features/dashboard/pages/dashboard_page.dart b/lib/src/features/dashboard/pages/dashboard_page.dart index d42ed95..a151411 100644 --- a/lib/src/features/dashboard/pages/dashboard_page.dart +++ b/lib/src/features/dashboard/pages/dashboard_page.dart @@ -15,6 +15,8 @@ import 'package:librarian_app/src/features/loans/pages/checkout_page.dart'; import 'package:librarian_app/src/features/loans/pages/loan_details_page.dart'; import 'package:librarian_app/src/features/loans/widgets/loans_list/searchable_loans_list.dart'; import 'package:librarian_app/src/features/loans/widgets/layouts/loans_desktop_layout.dart'; +import 'package:librarian_app/src/features/updates/widgets/update_dialog_controller.dart'; +import 'package:librarian_app/src/features/updates/notifiers/update_notifier.dart'; import 'package:librarian_app/src/utils/media_query.dart'; import 'package:librarian_app/src/features/actions/widgets/actions.dart' as librarian_actions; @@ -32,6 +34,16 @@ class DashboardPage extends ConsumerStatefulWidget { class _DashboardPageState extends ConsumerState { final _createButtonKey = GlobalKey(); + final _updateNotifier = UpdateNotifier.instance; + + @override + void initState() { + super.initState(); + _updateNotifier.addListener(() { + UpdateDialogController(context) + .showUpdateDialog(_updateNotifier.newerVersion!); + }); + } int _moduleIndex = 0; @@ -165,8 +177,10 @@ class _DashboardPageState extends ConsumerState { centerTitle: mobile, actions: [ if (!mobile) ...[ + _UpdateButton(), + const SizedBox(width: 32), const UserTray(), - const SizedBox(width: 16), + const SizedBox(width: 32), ], IconButton( onPressed: () { @@ -237,3 +251,27 @@ class DashboardModule { final Widget desktopLayout; final Widget? mobileLayout; } + +class _UpdateButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: UpdateNotifier.instance, + builder: (context, _) { + final newVersion = UpdateNotifier.instance.newerVersion; + + if (newVersion == null) { + return const SizedBox.shrink(); + } + + return IconButton( + onPressed: () { + UpdateDialogController(context).showUpdateDialog(newVersion); + }, + tooltip: 'Update Available', + icon: const Icon(Icons.update, color: Colors.amber), + ); + }, + ); + } +} diff --git a/lib/src/features/loans/widgets/checkout/checkout_stepper.dart b/lib/src/features/loans/widgets/checkout/checkout_stepper.dart index a34e681..60e5f5e 100644 --- a/lib/src/features/loans/widgets/checkout/checkout_stepper.dart +++ b/lib/src/features/loans/widgets/checkout/checkout_stepper.dart @@ -113,26 +113,12 @@ class _CheckoutStepperState extends ConsumerState { subtitle: _borrower != null ? Text(_borrower!.name) : null, content: Column( children: [ - TextField( - controller: TextEditingController(text: _borrower?.name), - canRequestFocus: false, - decoration: const InputDecoration( - labelText: 'Borrower', - prefixIcon: Icon(Icons.person_rounded), - ), - onTap: () { - ref.invalidate(borrowersRepositoryProvider); - ref.read(borrowersRepositoryProvider).then((borrowers) async { - return await showSearch( - context: context, - delegate: BorrowerSearchDelegate(borrowers), - useRootNavigator: true, - ); - }).then((borrower) { - if (borrower != null) { - setState(() => _borrower = borrower); - } - }); + _SelectBorrowerTextField( + text: _borrower?.name, + onSelected: (borrower) { + if (borrower != null) { + setState(() => _borrower = borrower); + } }, ), if (_borrower != null && !_borrower!.active) ...[ @@ -229,3 +215,50 @@ class _CheckoutStepperState extends ConsumerState { ); } } + +class _SelectBorrowerTextField extends ConsumerStatefulWidget { + const _SelectBorrowerTextField({ + required this.text, + required this.onSelected, + }); + + final String? text; + final void Function(BorrowerModel? borrower) onSelected; + + @override + ConsumerState<_SelectBorrowerTextField> createState() => + _SelectBorrowerTextFieldState(); +} + +class _SelectBorrowerTextFieldState + extends ConsumerState<_SelectBorrowerTextField> { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return TextField( + controller: TextEditingController(text: widget.text), + canRequestFocus: false, + decoration: InputDecoration( + labelText: _isLoading ? 'Loading...' : 'Borrower', + prefixIcon: const Icon(Icons.person_rounded), + ), + enabled: !_isLoading, + onTap: () { + setState(() => _isLoading = true); + + ref.invalidate(borrowersRepositoryProvider); + ref.read(borrowersRepositoryProvider).then((borrowers) async { + return await showSearch( + context: context, + delegate: BorrowerSearchDelegate(borrowers), + useRootNavigator: true, + ); + }).then((borrower) { + widget.onSelected(borrower); + setState(() => _isLoading = false); + }); + }, + ); + } +} diff --git a/lib/src/features/loans/widgets/checkout/connected_thing_search_field.dart b/lib/src/features/loans/widgets/checkout/connected_thing_search_field.dart index c67496e..f815ce5 100644 --- a/lib/src/features/loans/widgets/checkout/connected_thing_search_field.dart +++ b/lib/src/features/loans/widgets/checkout/connected_thing_search_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:librarian_app/src/features/inventory/data/inventory_repository.dart'; import 'package:librarian_app/src/features/inventory/models/item_model.dart'; @@ -29,15 +30,17 @@ class ConnectedThingSearchField extends StatelessWidget { controller: _textController, onSubmitted: (_) => _submit(), decoration: InputDecoration( - hintText: 'Enter Thing #', - prefixText: '#', - prefixIcon: const Icon(Icons.build_rounded), - suffix: IconButton( + hintText: 'Enter Thing Number', + prefixIcon: const Icon(Icons.numbers), + suffixIcon: IconButton( tooltip: 'Add Thing', onPressed: () => _submit(), icon: const Icon(Icons.add_rounded), ), ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], ); } } diff --git a/lib/src/features/splash/pages/splash_page.dart b/lib/src/features/splash/pages/splash_page.dart index 0d622eb..4ce2ef7 100644 --- a/lib/src/features/splash/pages/splash_page.dart +++ b/lib/src/features/splash/pages/splash_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:librarian_app/src/features/authentication/pages/signin_page.dart'; import 'package:librarian_app/src/features/authentication/providers/auth_service_provider.dart'; import 'package:librarian_app/src/features/dashboard/pages/dashboard_page.dart'; +import 'package:librarian_app/src/widgets/fade_page_route.dart'; class SplashPage extends ConsumerStatefulWidget { const SplashPage({super.key}); @@ -22,12 +23,12 @@ class _SplashPageState extends ConsumerState { if (!service.hasValidSession) { Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const SignInPage()), + createFadePageRoute(child: const SignInPage()), (route) => false, ); } else { Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const DashboardPage()), + createFadePageRoute(child: const DashboardPage()), (route) => false, ); } diff --git a/lib/src/features/updates/core/semantic_version.dart b/lib/src/features/updates/core/semantic_version.dart new file mode 100644 index 0000000..acccdef --- /dev/null +++ b/lib/src/features/updates/core/semantic_version.dart @@ -0,0 +1,57 @@ +class SemanticVersion { + const SemanticVersion({ + required this.major, + required this.minor, + required this.patch, + required this.build, + }); + + final int major; + final int minor; + final int patch; + final int build; + + String get text => '$major.$minor.$patch+$build'; + + bool isNewerThan(SemanticVersion other) { + if (major > other.major) { + return true; + } + + if (major == other.major && minor > other.minor) { + return true; + } + + if (major == other.major && minor == other.minor && patch > other.patch) { + return true; + } + + if (major == other.major && + minor == other.minor && + patch == other.patch && + build > other.build) { + return true; + } + + return false; + } + + bool isSameAs(SemanticVersion other) { + return major == other.major && + minor == other.minor && + patch == other.patch && + build == other.build; + } + + factory SemanticVersion.from(String stringValue, String buildString) { + final segments = stringValue.split('.'); + final numSegments = segments.length; + + return SemanticVersion( + major: numSegments > 0 ? int.parse(segments[0]) : 0, + minor: numSegments > 1 ? int.parse(segments[1]) : 0, + patch: numSegments > 2 ? int.parse(segments[2]) : 0, + build: int.parse(buildString), + ); + } +} diff --git a/lib/src/features/updates/notifiers/update_notifier.dart b/lib/src/features/updates/notifiers/update_notifier.dart new file mode 100644 index 0000000..3297687 --- /dev/null +++ b/lib/src/features/updates/notifiers/update_notifier.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +import '../core/semantic_version.dart'; + +class UpdateNotifier extends ChangeNotifier { + // ignore: unused_field + late final Timer _timer; + late final SemanticVersion version; + + SemanticVersion? newerVersion; + + UpdateNotifier() { + fetchVersionAndBuildNumber().then((result) { + version = result; + + if (kDebugMode) { + print('Loaded version: ${result.text}'); + } + }); + + const timerDuration = + kDebugMode ? Duration(seconds: 10) : Duration(hours: 3); + + _timer = Timer.periodic(timerDuration, (timer) { + if (kDebugMode) { + print('Checking for new version...'); + } + + fetchVersionAndBuildNumber().then((result) { + if (!kDebugMode && !result.isNewerThan(version)) { + return; + } + + if (kDebugMode) { + print('New version detected: ${result.text}'); + _timer.cancel(); + } + + newerVersion = result; + notifyListeners(); + }); + }); + } + + Future fetchVersionAndBuildNumber() async { + final versionUri = Uri.base.removeFragment().replace(path: '/version.json'); + final response = await Dio().getUri(versionUri); + final data = jsonDecode(response.data) as Map; + + return SemanticVersion.from( + data['version'] as String, data['build_number'] as String); + } + + static UpdateNotifier? _instance; + + static UpdateNotifier get instance { + if (_instance != null) { + return _instance!; + } + + _instance = UpdateNotifier(); + return _instance!; + } +} diff --git a/lib/src/features/updates/widgets/update_dialog.dart b/lib/src/features/updates/widgets/update_dialog.dart new file mode 100644 index 0000000..2bb2168 --- /dev/null +++ b/lib/src/features/updates/widgets/update_dialog.dart @@ -0,0 +1,38 @@ +// ignore: avoid_web_libraries_in_flutter +import 'dart:html'; + +import 'package:flutter/material.dart'; + +import '../core/semantic_version.dart'; + +class UpdateDialog extends StatelessWidget { + const UpdateDialog({super.key, required this.version}); + + final SemanticVersion version; + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: const Icon(Icons.update), + title: const Text('Update Available'), + content: Text( + 'A new version (${version.text}) is available.\n\nWarning: Choosing \'Update\' will restart the app and delete any unsaved changes. You may update at any time by restarting the app.'), + contentPadding: const EdgeInsets.all(16), + actions: [ + OutlinedButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () { + // Restart the app + window.location.reload(); + }, + child: const Text('Update'), + ), + ], + ); + } +} diff --git a/lib/src/features/updates/widgets/update_dialog_controller.dart b/lib/src/features/updates/widgets/update_dialog_controller.dart new file mode 100644 index 0000000..61818b5 --- /dev/null +++ b/lib/src/features/updates/widgets/update_dialog_controller.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +import '../core/semantic_version.dart'; +import 'update_dialog.dart'; + +class UpdateDialogController { + const UpdateDialogController(this.context); + + final BuildContext context; + + void showUpdateDialog(SemanticVersion version) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => FractionallySizedBox( + widthFactor: 0.3, + child: UpdateDialog(version: version), + ), + ); + } +} diff --git a/lib/src/services/image_service.dart b/lib/src/services/image_service.dart index a4a50a3..ea9fb5d 100644 --- a/lib/src/services/image_service.dart +++ b/lib/src/services/image_service.dart @@ -21,6 +21,10 @@ class ImageService { return ImageUploadResult(url: url); } + + String getPublicUrl(String bucket, String path) { + return Supabase.instance.client.storage.from(bucket).getPublicUrl(path); + } } class ImageUploadResult { diff --git a/lib/src/theme/indigo_theme.dart b/lib/src/theme/indigo_theme.dart index fbdb99d..7723fc3 100644 --- a/lib/src/theme/indigo_theme.dart +++ b/lib/src/theme/indigo_theme.dart @@ -1,8 +1,20 @@ import 'package:flutter/material.dart'; -final indigoTheme = ThemeData( - primarySwatch: Colors.indigo, - primaryColor: Colors.deepPurple, - brightness: Brightness.dark, - useMaterial3: true, -); +ThemeData _createIndigoTheme() { + final baseTheme = ThemeData( + primarySwatch: Colors.indigo, + primaryColor: Colors.deepPurple, + brightness: Brightness.dark, + useMaterial3: true, + ); + + return baseTheme.copyWith( + listTileTheme: ListTileThemeData( + selectedTileColor: + baseTheme.colorScheme.secondaryContainer.withAlpha(100), + selectedColor: Colors.white, + ), + ); +} + +final indigoTheme = _createIndigoTheme(); diff --git a/lib/src/widgets/fade_page_route.dart b/lib/src/widgets/fade_page_route.dart new file mode 100644 index 0000000..9ef20ef --- /dev/null +++ b/lib/src/widgets/fade_page_route.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +Route createFadePageRoute({required Widget child}) { + return PageRouteBuilder( + pageBuilder: (_, __, ___) => child, + transitionsBuilder: (_, a, __, c) => FadeTransition( + opacity: a, + child: c, + ), + ); +} diff --git a/pubspec.lock b/pubspec.lock index a122c07..64d6abb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -440,6 +440,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -460,26 +484,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -500,10 +524,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: @@ -957,22 +981,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive description: - name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "1.1.0" - web: + version: "13.0.0" + watcher: dependency: transitive description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.0" web_socket_channel: dependency: transitive description: @@ -1046,5 +1070,5 @@ packages: source: hosted version: "1.1.1" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" + dart: ">=3.2.0-0 <4.0.0" flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5e95f31..5acaa9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: librarian_app -description: A new Flutter project. +description: Simply manage your library of things. # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. @@ -10,14 +10,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +version: 1.0.0+2 environment: sdk: '>=3.0.0' diff --git a/web/index.html b/web/index.html index f02b79c..b749293 100644 --- a/web/index.html +++ b/web/index.html @@ -18,18 +18,18 @@ - + - + - librarian_app + Librarian