tane/apps/app_seeds/lib/ui/about_screen.dart

212 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../i18n/strings.g.dart';
import 'theme.dart';
/// The app's public website. Shown as a tappable row in [AboutScreen].
const String _websiteUrl = 'https://tane.comunes.org';
/// The year Tane's copyright starts. Ğ1nkgo uses its own first year (2023);
/// Tane's is 2026.
const int _copyrightStartYear = 2026;
/// A single year (`2026`) until the calendar rolls over, then a range
/// (`2026-2027`), mirroring Ğ1nkgo's legalese line.
String _copyrightYears() {
final int now = DateTime.now().year;
return now <= _copyrightStartYear
? '$_copyrightStartYear'
: '$_copyrightStartYear-$now';
}
/// An "about" screen à la Ğ1nkgo: brand header, the human explanation of what
/// Tane is and where its name comes from, the app version, license, the
/// website, and the bundled open-source licenses page.
class AboutScreen extends StatelessWidget {
const AboutScreen({super.key});
@override
Widget build(BuildContext context) {
final t = context.t;
return Scaffold(
appBar: AppBar(title: Text(t.about.title)),
body: ListView(
padding: const EdgeInsets.only(bottom: 24),
children: [
const _Header(),
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 8),
child: Text(
t.about.tagline,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: seedOnSurface,
height: 1.35,
),
),
),
const _Paragraph(),
const Divider(height: 32),
_InfoTile(
icon: Icons.info_outline,
label: t.about.version,
valueBuilder: (context) => const _VersionText(),
),
_InfoTile(
icon: Icons.balance_outlined,
label: t.about.license,
value: t.about.licenseValue,
),
ListTile(
leading: const Icon(Icons.public, color: seedGreen),
title: Text(t.about.website),
subtitle: const Text(_websiteUrl),
onTap: () => launchUrl(
Uri.parse(_websiteUrl),
mode: LaunchMode.externalApplication,
),
),
ListTile(
leading: const Icon(Icons.privacy_tip_outlined, color: seedGreen),
title: Text(t.legal.title),
subtitle: Text(t.legal.subtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => context.push('/legal'),
),
ListTile(
leading: const Icon(Icons.description_outlined, color: seedGreen),
title: Text(t.about.openSourceLicenses),
subtitle: Text(t.about.openSourceLicensesSubtitle),
onTap: () => showLicensePage(
context: context,
applicationName: t.app.title,
applicationLegalese: t.about.copyright(years: _copyrightYears()),
applicationIcon: Padding(
padding: const EdgeInsets.all(8),
child: Image.asset('assets/logo.png', width: 48),
),
),
),
const Divider(height: 32),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
t.about.copyright(years: _copyrightYears()),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: seedOnSurface.withValues(alpha: 0.7),
),
),
),
],
),
);
}
}
/// Brand block: the logo, the app name and its 種 kanji.
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
final t = context.t;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 4),
child: Column(
children: [
Image.asset('assets/logo.png', width: 88),
const SizedBox(height: 12),
Text(
t.app.title,
style: const TextStyle(
color: seedGreen,
fontSize: 28,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
Text(
t.about.kanji,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: seedGreen.withValues(alpha: 0.7),
),
),
],
),
);
}
}
/// The two explanatory paragraphs: what Tane is, and the heritage of its
/// name (yui / tanomoshi / the paper Plantare).
class _Paragraph extends StatelessWidget {
const _Paragraph();
@override
Widget build(BuildContext context) {
final t = context.t;
final style = Theme.of(
context,
).textTheme.bodyMedium?.copyWith(color: seedOnSurface, height: 1.5);
return Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(t.about.intro, style: style),
const SizedBox(height: 14),
Text(t.about.heritage, style: style),
],
),
);
}
}
/// A leading-icon row showing a static [value] or a [valueBuilder] widget.
class _InfoTile extends StatelessWidget {
const _InfoTile({
required this.icon,
required this.label,
this.value,
this.valueBuilder,
});
final IconData icon;
final String label;
final String? value;
final WidgetBuilder? valueBuilder;
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon, color: seedGreen),
title: Text(label),
trailing: value != null
? Text(value!, style: Theme.of(context).textTheme.bodyMedium)
: valueBuilder?.call(context),
);
}
}
/// Reads the app version from the platform and renders it as `x.y.z (build)`.
class _VersionText extends StatelessWidget {
const _VersionText();
@override
Widget build(BuildContext context) {
return FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
final info = snapshot.data;
final text = info == null
? ''
: '${info.version} (${info.buildNumber})';
return Text(text, style: Theme.of(context).textTheme.bodyMedium);
},
);
}
}