Add About OOTT page with version, links, and legal notices

Implements the /about route with app description, version info,
clickable links to the source repo and AGPL-3.0 license, and
third-party copyright notices. Adds url_launcher for cross-platform
link handling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-28 17:29:55 -04:00
co-authored by Claude Sonnet 4.6
parent 93f40b892d
commit 97e4bb1000
9 changed files with 371 additions and 1 deletions
+293
View File
@@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
const _version = '0.1.0';
const _releaseDate = 'May 28, 2026';
const _repoUrl = 'https://github.com/rzuasti/oott';
const _licenseUrl = 'https://www.gnu.org/licenses/agpl-3.0.html';
const _licenseName = 'GNU Affero General Public License v3 (AGPL-3.0)';
class About extends StatelessWidget {
const About({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(title: const Text('About OOTT')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Easy to setup and use network device discovery and alert system. '
'Notifies you when new or unknown devices join your local area network.',
style: textTheme.bodyLarge,
),
const SizedBox(height: 8),
Text(
'v$_version - released $_releaseDate',
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 24),
_SurfaceContainer(
colorScheme: colorScheme,
child: Column(
children: [
_LinkRow(
icon: Icons.code,
label: 'Source code',
url: _repoUrl,
colorScheme: colorScheme,
textTheme: textTheme,
),
Divider(
height: 1,
color: colorScheme.outlineVariant,
indent: 16,
endIndent: 16,
),
_LicenseRow(colorScheme: colorScheme, textTheme: textTheme),
],
),
),
const SizedBox(height: 16),
_SurfaceContainer(
colorScheme: colorScheme,
padding: const EdgeInsets.all(16),
child: _NoticesSection(textTheme: textTheme),
),
],
),
),
);
}
}
class _SurfaceContainer extends StatelessWidget {
const _SurfaceContainer({
required this.colorScheme,
required this.child,
this.padding,
});
final ColorScheme colorScheme;
final Widget child;
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(12),
),
padding: padding,
child: child,
);
}
}
class _LinkRow extends StatelessWidget {
const _LinkRow({
required this.icon,
required this.label,
required this.url,
required this.colorScheme,
required this.textTheme,
});
final IconData icon;
final String label;
final String url;
final ColorScheme colorScheme;
final TextTheme textTheme;
Future<void> _open() async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _open,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Row(
children: [
Icon(icon, size: 18, color: colorScheme.primary),
const SizedBox(width: 12),
Text(
label,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: colorScheme.primary,
),
),
],
),
),
);
}
}
class _LicenseRow extends StatelessWidget {
const _LicenseRow({required this.colorScheme, required this.textTheme});
final ColorScheme colorScheme;
final TextTheme textTheme;
Future<void> _open() async {
final uri = Uri.parse(_licenseUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.gavel, size: 18, color: colorScheme.primary),
const SizedBox(width: 12),
Flexible(
child: Wrap(
children: [
Text('Licensed under ', style: textTheme.bodyMedium),
InkWell(
onTap: _open,
child: Text(
_licenseName,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.primary,
decoration: TextDecoration.underline,
decorationColor: colorScheme.primary,
),
),
),
],
),
),
],
),
);
}
}
class _NoticesSection extends StatelessWidget {
const _NoticesSection({required this.textTheme});
final TextTheme textTheme;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'OOTT, Copyright (C) 2024-2026 Ricardo Zuasti',
style: textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'This product includes software developed by third parties and distributed '
'under the Apache License, Version 2.0.',
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
..._thirdPartyComponents.map(
(c) => _ThirdPartyEntry(component: c, textTheme: textTheme),
),
],
);
}
}
class _ThirdPartyEntry extends StatelessWidget {
const _ThirdPartyEntry({required this.component, required this.textTheme});
final _ThirdPartyComponent component;
final TextTheme textTheme;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
component.name,
style: textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
),
Text(
component.copyright,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
SelectableText(
component.url,
style: textTheme.bodySmall?.copyWith(color: colorScheme.primary),
),
],
),
);
}
}
class _ThirdPartyComponent {
const _ThirdPartyComponent({
required this.name,
required this.copyright,
required this.url,
});
final String name;
final String copyright;
final String url;
}
const _thirdPartyComponents = [
_ThirdPartyComponent(
name: 'openssl',
copyright:
'Copyright 2011-2017 Google Inc., 2013 Jack Lloyd, 2013-2014 Steven Fackler. Apache-2.0.',
url: 'https://github.com/rust-openssl/rust-openssl',
),
_ThirdPartyComponent(
name: 'rusqlite_migration',
copyright: 'Copyright Clément Joly and contributors. Apache-2.0.',
url: 'https://github.com/cljoly/rusqlite_migration',
),
_ThirdPartyComponent(
name: 'sync_wrapper',
copyright: 'Copyright Actyx AG. Apache-2.0.',
url: 'https://github.com/Actyx/sync_wrapper',
),
_ThirdPartyComponent(
name: 'zopfli',
copyright:
'Copyright the zopfli-rs contributors. Based on the original Zopfli C code by Google. Apache-2.0.',
url: 'https://github.com/zopfli-rs/zopfli',
),
];
+2 -1
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:frontend/about/about.dart';
import 'package:frontend/settings/settings.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -47,7 +48,7 @@ final GoRouter router = GoRouter(
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => const Placeholder(),
builder: (context, state) => const About(),
),
],
),
@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include <encrypter/encrypter_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) encrypter_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "EncrypterPlugin");
encrypter_plugin_register_with_registrar(encrypter_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
encrypter
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
@@ -7,8 +7,10 @@ import Foundation
import encrypter
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
EncrypterPlugin.register(with: registry.registrar(forPlugin: "EncrypterPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
+64
View File
@@ -589,6 +589,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c"
url: "https://pub.dev"
source: hosted
version: "6.3.30"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
vector_math:
dependency: transitive
description:
+1
View File
@@ -18,6 +18,7 @@ dependencies:
infinite_scroll_pagination: ^5.1.1
fl_chart: ^1.2.0
google_fonts: ^6.2.1
url_launcher: ^6.3.0
dev_dependencies:
flutter_test:
@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <encrypter/encrypter_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
EncrypterPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("EncrypterPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
encrypter
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST