diff --git a/lib/messages.i18n.dart b/lib/messages.i18n.dart index 38e98df..1a4be51 100644 --- a/lib/messages.i18n.dart +++ b/lib/messages.i18n.dart @@ -175,6 +175,27 @@ class HomeMessages { /// "Failed to load houses." /// ``` String get failedToLoadHouses => """Failed to load houses."""; + + /// ```dart + /// "Pantry is not installed" + /// ``` + String get serverAppMissingTitle => """Pantry is not installed"""; + + /// ```dart + /// "This app is a client for the Pantry app on Nextcloud. It looks like Pantry isn't installed on your server yet. Ask your administrator to install it from the Nextcloud app store, or install it yourself if you have admin access." + /// ``` + String get serverAppMissingBody => + """This app is a client for the Pantry app on Nextcloud. It looks like Pantry isn't installed on your server yet. Ask your administrator to install it from the Nextcloud app store, or install it yourself if you have admin access."""; + + /// ```dart + /// "Open Nextcloud apps" + /// ``` + String get openAppStore => """Open Nextcloud apps"""; + + /// ```dart + /// "Learn more" + /// ``` + String get learnMore => """Learn more"""; } class NavMessages { @@ -860,6 +881,11 @@ Please complete login in your browser.""", """login.loginFailed""": """Login failed. Please try again.""", """home.noHouses""": """No houses found. Create one in Nextcloud first.""", """home.failedToLoadHouses""": """Failed to load houses.""", + """home.serverAppMissingTitle""": """Pantry is not installed""", + """home.serverAppMissingBody""": + """This app is a client for the Pantry app on Nextcloud. It looks like Pantry isn't installed on your server yet. Ask your administrator to install it from the Nextcloud app store, or install it yourself if you have admin access.""", + """home.openAppStore""": """Open Nextcloud apps""", + """home.learnMore""": """Learn more""", """nav.checklists""": """Checklists""", """nav.photoBoard""": """Photo Board""", """nav.notesWall""": """Notes Wall""", diff --git a/lib/messages.i18n.yaml b/lib/messages.i18n.yaml index 07b55aa..9abf8ac 100644 --- a/lib/messages.i18n.yaml +++ b/lib/messages.i18n.yaml @@ -20,6 +20,10 @@ login: home: noHouses: No houses found. Create one in Nextcloud first. failedToLoadHouses: Failed to load houses. + serverAppMissingTitle: Pantry is not installed + serverAppMissingBody: "This app is a client for the Pantry app on Nextcloud. It looks like Pantry isn't installed on your server yet. Ask your administrator to install it from the Nextcloud app store, or install it yourself if you have admin access." + openAppStore: Open Nextcloud apps + learnMore: Learn more nav: checklists: Checklists diff --git a/lib/views/home/home_controller.dart b/lib/views/home/home_controller.dart index 4a17a26..0018405 100644 --- a/lib/views/home/home_controller.dart +++ b/lib/views/home/home_controller.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:pantry/i18n.dart'; import 'package:pantry/models/house.dart'; +import 'package:pantry/services/api_client.dart'; import 'package:pantry/services/house_service.dart'; import 'package:pantry/services/prefs_service.dart'; @@ -17,8 +18,14 @@ class HomeController extends ChangeNotifier { String? _error; String? get error => _error; + /// True when the Pantry server app is not installed on the user's + /// Nextcloud instance (API returns 404). + bool _serverAppMissing = false; + bool get serverAppMissing => _serverAppMissing; + Future load() async { _error = null; + _serverAppMissing = false; // Restore from cache final cached = HouseService.instance.getCached(); @@ -52,7 +59,11 @@ class HomeController extends ChangeNotifier { } catch (e) { debugPrint('[HomeController] Failed to load houses: $e'); if (_houses.isEmpty) { - _error = m.home.failedToLoadHouses; + if (e is ApiException && e.statusCode == 404) { + _serverAppMissing = true; + } else { + _error = m.home.failedToLoadHouses; + } _isLoading = false; notifyListeners(); } diff --git a/lib/views/home/home_view.dart b/lib/views/home/home_view.dart index 9393355..fadd249 100644 --- a/lib/views/home/home_view.dart +++ b/lib/views/home/home_view.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:pantry/i18n.dart'; import 'package:pantry/models/house.dart'; @@ -105,6 +106,10 @@ class _HomeViewBodyState extends State<_HomeViewBody> { return const Center(child: CircularProgressIndicator()); } + if (controller.serverAppMissing) { + return _ServerAppMissingView(onRetry: controller.load); + } + if (controller.error != null) { return Center( child: Padding( @@ -241,3 +246,70 @@ class _UserMenuButton extends StatelessWidget { ); } } + +class _ServerAppMissingView extends StatelessWidget { + final VoidCallback onRetry; + + const _ServerAppMissingView({required this.onRetry}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final serverUrl = AuthService.instance.credentials?.serverUrl ?? ''; + // Nextcloud app store path for pantry + final appUrl = '$serverUrl/settings/apps/organization/pantry'; + final infoUrl = 'https://apps.nextcloud.com/apps/pantry'; + + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.extension_off_outlined, + size: 72, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 24), + Text( + m.home.serverAppMissingTitle, + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + m.home.serverAppMissingBody, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + FilledButton.icon( + onPressed: () => _launch(appUrl), + icon: const Icon(Icons.open_in_new), + label: Text(m.home.openAppStore), + ), + const SizedBox(height: 8), + TextButton.icon( + onPressed: () => _launch(infoUrl), + icon: const Icon(Icons.info_outline), + label: Text(m.home.learnMore), + ), + const SizedBox(height: 16), + TextButton(onPressed: onRetry, child: Text(m.common.retry)), + ], + ), + ), + ); + } + + Future _launch(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } +}