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/