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 public source repository (AGPL). Shown as a tappable row in [AboutScreen]. const String _sourceUrl = 'https://git.comunes.org/comunes/tane'; /// The translation platform where anyone can help translate Tane. const String _translateUrl = 'https://translate.comunes.org/projects/tane/'; /// 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.code, color: seedGreen), title: Text(t.about.sourceCode), subtitle: const Text(_sourceUrl), onTap: () => launchUrl( Uri.parse(_sourceUrl), mode: LaunchMode.externalApplication, ), ), ListTile( leading: const Icon(Icons.translate, color: seedGreen), title: Text(t.about.translate), subtitle: Text(t.about.translateSubtitle), onTap: () => launchUrl( Uri.parse(_translateUrl), 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( 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); }, ); } }