204 lines
6.1 KiB
Dart
204 lines
6.1 KiB
Dart
import 'package:flutter/material.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://tanemaki.app';
|
|
|
|
/// The year Tanemaki's copyright starts. Ğ1nkgo uses its own first year (2023);
|
|
/// Tanemaki'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
|
|
/// Tanemaki 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.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 Tanemaki 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);
|
|
},
|
|
);
|
|
}
|
|
}
|