diff --git a/assets/2.0x/app-store.png b/assets/2.0x/app-store.png new file mode 100644 index 000000000..537eb9abd Binary files /dev/null and b/assets/2.0x/app-store.png differ diff --git a/assets/app-store.png b/assets/app-store.png new file mode 100644 index 000000000..950cee204 Binary files /dev/null and b/assets/app-store.png differ diff --git a/assets/app-store.svg b/assets/app-store.svg new file mode 100644 index 000000000..a3cff0cd7 --- /dev/null +++ b/assets/app-store.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/about.dart b/lib/about.dart index 1aac2507a..ec5b66834 100644 --- a/lib/about.dart +++ b/lib/about.dart @@ -1 +1 @@ -export 'src/about/about_dialog.dart'; +export 'src/about/about_page.dart'; diff --git a/lib/src/widgets/constants.dart b/lib/constants.dart similarity index 88% rename from lib/src/widgets/constants.dart rename to lib/constants.dart index 1bd8eb2b2..37bd2441a 100644 --- a/lib/src/widgets/constants.dart +++ b/lib/constants.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +const kAppName = 'App Store'; +const kGitHubRepo = 'ubuntu/software'; + const kNaviRailWidth = 205.0; const kGridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( diff --git a/lib/src/about/about_dialog.dart b/lib/src/about/about_dialog.dart deleted file mode 100644 index e90566a93..000000000 --- a/lib/src/about/about_dialog.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:yaru_icons/yaru_icons.dart'; -import 'package:yaru_widgets/yaru_widgets.dart'; - -import '/l10n.dart'; -import 'about_provider.dart'; - -Future showAboutDialog(BuildContext context) { - return showDialog( - context: context, - builder: (_) => const AboutDialog(), - ); -} - -class AboutDialog extends ConsumerWidget { - const AboutDialog({super.key}); - - static IconData get icon => YaruIcons.question; - static String label(BuildContext context) => - AppLocalizations.of(context).aboutDialogLabel; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final l10n = AppLocalizations.of(context); - return AlertDialog( - clipBehavior: Clip.antiAlias, - titlePadding: EdgeInsets.zero, - contentPadding: EdgeInsets.zero, - title: YaruDialogTitleBar(title: Text(l10n.aboutDialogTitle)), - content: const SizedBox( - width: 500, - child: _ContributorListView(repo: 'ubuntu/software'), - ), - ); - } -} - -class _ContributorListView extends ConsumerWidget { - const _ContributorListView({required this.repo}); - - final String repo; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(contributorsProvider(repo)); - return state.when( - data: (contributors) => ListView.separated( - padding: const EdgeInsets.all(kYaruPagePadding), - itemCount: contributors.length, - itemBuilder: (context, index) { - final contributor = contributors[index]; - return ListTile( - leading: CircleAvatar( - backgroundImage: contributor.avatarUrl != null - ? NetworkImage(contributor.avatarUrl!) - : null, - ), - title: Text(contributor.login ?? ''), - ); - }, - separatorBuilder: (_, __) => const SizedBox(height: kYaruPagePadding), - ), - error: (error, stackTrace) => ErrorWidget(error), - loading: () => const Center(child: YaruCircularProgressIndicator()), - ); - } -} diff --git a/lib/src/about/about_page.dart b/lib/src/about/about_page.dart new file mode 100644 index 000000000..1d71aa29e --- /dev/null +++ b/lib/src/about/about_page.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:yaru_icons/yaru_icons.dart'; +import 'package:yaru_widgets/yaru_widgets.dart'; + +import '/constants.dart'; +import '/l10n.dart'; +import 'about_providers.dart'; + +class AboutPage extends StatelessWidget { + const AboutPage({super.key}); + + static IconData icon(bool selected) => + selected ? YaruIcons.question_filled : YaruIcons.question; + static String label(BuildContext context) => + AppLocalizations.of(context).aboutPageLabel; + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + const SliverPadding( + padding: EdgeInsets.all(kYaruPagePadding), + sliver: SliverToBoxAdapter(child: _AboutHeader()), + ), + SliverPadding( + padding: const EdgeInsets.all(kYaruPagePadding), + sliver: SliverToBoxAdapter( + child: Align( + alignment: AlignmentDirectional.topStart, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 500), + child: const _ContributorView(repo: kGitHubRepo), + ), + ), + ), + ), + const SliverPadding( + padding: EdgeInsets.all(kYaruPagePadding), + sliver: SliverToBoxAdapter(child: _CommunityView()), + ), + const SliverFillRemaining( + hasScrollBody: false, + child: Padding( + padding: EdgeInsets.all(kYaruPagePadding), + child: _AboutFooter(), + ), + ), + ], + ); + } +} + +class _AboutHeader extends ConsumerWidget { + const _AboutHeader(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset('assets/app-store.png'), + const SizedBox(width: 32), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kAppName, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + ref.watch(versionProvider).maybeWhen( + data: (v) => Text(l10n.aboutPageVersionLabel(v)), + orElse: () => const SizedBox.shrink(), + ), + ], + ), + ], + ); + } +} + +class _AboutFooter extends StatelessWidget { + const _AboutFooter(); + + @override + Widget build(BuildContext context) { + // TODO: terms and conditions, privacy policy + return Align( + alignment: AlignmentDirectional.bottomStart, + child: MarkdownBody( + data: '©️ ${DateTime.now().year} Canonical Ltd.', + ), + ); + } +} + +class _ContributorView extends ConsumerWidget { + const _ContributorView({required this.repo}); + + final String repo; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final l10n = AppLocalizations.of(context); + final state = ref.watch(contributorsProvider(repo)); + final light = Theme.of(context).brightness == Brightness.light; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.aboutPageContributorTitle), + const SizedBox(height: 8), + state.when( + data: (contributors) => _ContributorWrap(contributors), + error: (error, stackTrace) => ErrorWidget(error), + loading: () => Shimmer.fromColors( + baseColor: light ? kShimmerBaseLight : kShimmerBaseDark, + highlightColor: + light ? kShimmerHighLightLight : kShimmerHighLightDark, + child: _ContributorWrap(List.filled(36, null)), + ), + ), + ], + ); + } +} + +class _ContributorWrap extends StatelessWidget { + const _ContributorWrap(this.contributors); + + final List contributors; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final contributor in contributors) + Tooltip( + message: contributor?.login ?? '', + child: InkWell( + customBorder: const CircleBorder(), + onTap: contributor?.htmlUrl != null + ? () => launchUrlString(contributor?.htmlUrl ?? '') + : null, + child: CircleAvatar( + radius: 16, + backgroundImage: contributor?.avatarUrl != null + ? NetworkImage(contributor!.avatarUrl!) + : null, + ), + ), + ), + ], + ); + } +} + +class _CommunityView extends StatelessWidget { + const _CommunityView(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.aboutPageCommunityTitle), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: _CommunityTile( + title: l10n.aboutPageContributeLabel, + subtitle: l10n.aboutPageGitHubLabel, + href: 'https://github.com/$kGitHubRepo', + ), + ), + const SizedBox(width: 16), + Expanded( + child: _CommunityTile( + title: l10n.aboutPagePublishLabel, + subtitle: l10n.aboutPageLearnMoreLabel, + href: 'https://snapcraft.io/docs/snapcraft', + ), + ), + ], + ), + ], + ); + } +} + +class _CommunityTile extends StatelessWidget { + const _CommunityTile({ + required this.title, + required this.subtitle, + required this.href, + }); + + final String title; + final String subtitle; + final String href; + + @override + Widget build(BuildContext context) { + return YaruTile( + // TODO: icon + leading: const Placeholder(fallbackWidth: 28, fallbackHeight: 28), + title: Text( + title, + overflow: TextOverflow.ellipsis, + ), + subtitle: MarkdownBody( + data: '[$subtitle]($href)', + onTapLink: (_, href, __) => launchUrlString(href!), + ), + padding: EdgeInsets.zero, + ); + } +} diff --git a/lib/src/about/about_provider.dart b/lib/src/about/about_provider.dart deleted file mode 100644 index 5cf0f9852..000000000 --- a/lib/src/about/about_provider.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:github/github.dart'; -import 'package:ubuntu_service/ubuntu_service.dart'; - -final contributorsProvider = - FutureProvider.autoDispose.family((ref, String repo) { - return getService() - .repositories - .listContributors(RepositorySlug.full(repo)) - .toList(); -}); diff --git a/lib/src/about/about_providers.dart b/lib/src/about/about_providers.dart new file mode 100644 index 000000000..826f40550 --- /dev/null +++ b/lib/src/about/about_providers.dart @@ -0,0 +1,31 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:github/github.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'package:ubuntu_service/ubuntu_service.dart'; + +final contributorsProvider = + FutureProvider.autoDispose.family((ref, String repo) async { + const designers = {'anasereijo', 'elioqoshi'}; + const exclude = {'weblate'}; + final contributors = await getService() + .repositories + .listContributors(RepositorySlug.full(repo)) + .where((c) => + c.type == 'User' && + !designers.contains(c.login) && + !exclude.contains(c.login)) + .toList(); + return [ + ...designers.map((d) => Contributor( + login: d, + htmlUrl: 'https://github.com/$d', + avatarUrl: 'https://avatars.githubusercontent.com/$d', + )), + ...contributors + ]; +}); + +final versionProvider = FutureProvider.autoDispose((ref) { + return PackageInfo.fromPlatform().then((info) => info.version); +}); diff --git a/lib/src/l10n/app_en.arb b/lib/src/l10n/app_en.arb index 32f86f4d6..49640b341 100644 --- a/lib/src/l10n/app_en.arb +++ b/lib/src/l10n/app_en.arb @@ -38,6 +38,19 @@ } } }, - "aboutDialogLabel": "About", - "aboutDialogTitle": "About App Store" + "aboutPageLabel": "About", + "aboutPageVersionLabel": "Version {version}", + "@aboutPageVersionLabel": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "aboutPageContributorTitle": "Designed and developed by:", + "aboutPageCommunityTitle": "Be part of the community:", + "aboutPageContributeLabel": "Contribute or report bug", + "aboutPageGitHubLabel": "Find us on GitHub", + "aboutPagePublishLabel": "Publish to the Snap Store", + "aboutPageLearnMoreLabel": "Learn more" } diff --git a/lib/src/store/store_app.dart b/lib/src/store/store_app.dart index 9fd38fd8f..b8acb9eab 100644 --- a/lib/src/store/store_app.dart +++ b/lib/src/store/store_app.dart @@ -3,11 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:yaru/yaru.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; -import '/about.dart'; +import '/constants.dart'; import '/detail.dart'; import '/l10n.dart'; import '/search.dart'; -import '/widgets.dart'; import 'store_navigator.dart'; import 'store_observer.dart'; import 'store_pages.dart'; @@ -77,15 +76,6 @@ class _StoreAppState extends ConsumerState { ), _ => null, }, - trailing: Builder( - builder: (context) => YaruNavigationRailItem( - icon: Icon(AboutDialog.icon), - label: Text(AboutDialog.label(context)), - style: YaruNavigationRailStyle.labelledExtended, - onTap: () => showAboutDialog(context), - width: kNaviRailWidth, - ), - ), ), ), ), diff --git a/lib/src/store/store_pages.dart b/lib/src/store/store_pages.dart index 08c4a3c8c..433a45bb1 100644 --- a/lib/src/store/store_pages.dart +++ b/lib/src/store/store_pages.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; +import '/about.dart'; import '/category.dart'; import '/explore.dart'; import '/manage.dart'; @@ -36,4 +37,9 @@ final pages = [ labelBuilder: ManagePage.label, builder: (_) => const ManagePage(), ), + ( + icon: AboutPage.icon, + labelBuilder: AboutPage.label, + builder: (_) => const AboutPage(), + ), ]; diff --git a/lib/src/widgets/snap_grid.dart b/lib/src/widgets/snap_grid.dart index 589215860..978bb4dc3 100644 --- a/lib/src/widgets/snap_grid.dart +++ b/lib/src/widgets/snap_grid.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:snapd/snapd.dart'; import 'package:yaru_widgets/constants.dart'; -import 'constants.dart'; +import '/constants.dart'; import 'snap_card.dart'; class SnapGrid extends StatelessWidget { diff --git a/lib/src/widgets/snap_icon.dart b/lib/src/widgets/snap_icon.dart index a3f6d29aa..87db46734 100644 --- a/lib/src/widgets/snap_icon.dart +++ b/lib/src/widgets/snap_icon.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:yaru_icons/yaru_icons.dart'; -import '/widgets.dart'; +import '/constants.dart'; import '/xdg_cache_manager.dart'; class SnapIcon extends StatelessWidget { diff --git a/lib/widgets.dart b/lib/widgets.dart index 1d5cd2cce..4741192ab 100644 --- a/lib/widgets.dart +++ b/lib/widgets.dart @@ -1,4 +1,3 @@ -export 'src/widgets/constants.dart'; export 'src/widgets/snap_card.dart'; export 'src/widgets/snap_grid.dart'; export 'src/widgets/snap_icon.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 9fa552435..52c92a0bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,6 @@ name: app_store description: App Store +version: 0.0.0-dev publish_to: 'none' environment: @@ -22,6 +23,7 @@ dependencies: gtk: ^2.1.0 handy_window: ^0.3.1 meta: ^1.9.1 + package_info_plus: ^4.0.2 path: ^1.8.3 shimmer: ^3.0.0 snapcraft_launcher: ^0.1.0 @@ -30,6 +32,7 @@ dependencies: ubuntu_service: ^0.2.2 ubuntu_test: ^0.1.0-0 ubuntu_widgets: ^0.1.2 + url_launcher: ^6.1.12 xdg_directories: ^1.0.0 yaru: ^0.9.0 yaru_icons: ^1.0.4 @@ -58,4 +61,4 @@ flutter: uses-material-design: true generate: true assets: - - assets/snap-store.desktop + - assets/