import 'dart:math'; import 'package:auto_route/auto_route.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../log.dart'; import '../../models/support.dart'; import '../../services/settings_service.dart'; import '../../state/init.dart'; import '../../state/settings.dart'; import '../app_router.dart'; import '../dialogs.dart'; const kHorizontalPadding = 16.0; class SettingsPage extends HookConsumerWidget { const SettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final l = AppLocalizations.of(context); // final downloads = ref.watch(downloadServiceProvider.select( // (value) => value.downloads, // )); return Scaffold( body: ListView( children: [ const SizedBox(height: 96), _SectionHeader(l.settingsServersName), const _Sources(), _SectionHeader(l.settingsNetworkName), const _Network(), _SectionHeader(l.settingsAboutName), _About(), // const _SectionHeader('Downloads'), // _Section( // children: downloads // .map( // (e) => ListTile( // isThreeLine: true, // title: Text(e.filename ?? e.url), // subtitle: Column( // mainAxisAlignment: MainAxisAlignment.start, // children: [ // Row(children: [Text('Progress: ${e.progress}%')]), // Row(children: [Text('Status: ${e.status})')]), // Text('Status: ${e.savedDir}'), // ], // ), // trailing: // CircularProgressIndicator(value: e.progress / 100), // ), // ) // .toList(), // ), ], ), ); } } class _Section extends StatelessWidget { final List children; const _Section({required this.children}); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ...children, const SizedBox(height: 32), ], ); } } class _SectionHeader extends StatelessWidget { final String title; const _SectionHeader(this.title); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( children: [ SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding), child: Text( title, style: theme.textTheme.displaySmall, ), ), ), const SizedBox(height: 12), ], ); } } class _Network extends StatelessWidget { const _Network(); @override Widget build(BuildContext context) { return const _Section( children: [ _OfflineMode(), _MaxBitrateWifi(), _MaxBitrateMobile(), _StreamFormat(), ], ); } } class _About extends HookConsumerWidget { _About(); final _homepage = Uri.parse('https://github.com/austinried/subtracks'); final _donate = Uri.parse('https://ko-fi.com/austinried'); @override Widget build(BuildContext context, WidgetRef ref) { final l = AppLocalizations.of(context); final pkg = ref.watch(packageInfoProvider).requireValue; return _Section( children: [ ListTile( title: const Text('subtracks'), subtitle: Text(l.settingsAboutVersion(pkg.version)), ), ListTile( title: Text(l.settingsAboutActionsLicenses), // trailing: const Icon(Icons.open_in_new_rounded), onTap: () {}, ), ListTile( title: Text(l.settingsAboutActionsProjectHomepage), subtitle: Text(_homepage.toString()), trailing: const Icon(Icons.open_in_new_rounded), onTap: () => launchUrl( _homepage, mode: LaunchMode.externalApplication, ), ), ListTile( title: Text(l.settingsAboutActionsSupport), subtitle: Text(_donate.toString()), trailing: const Icon(Icons.open_in_new_rounded), onTap: () => launchUrl( _donate, mode: LaunchMode.externalApplication, ), ), const SizedBox(height: 12), const _ShareLogsButton(), ], ); } } class _ShareLogsButton extends StatelessWidget { const _ShareLogsButton(); @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( icon: const Icon(Icons.share), label: Text(l.settingsAboutShareLogs), onPressed: () async { final files = await logFiles(); if (files.isEmpty) return; // ignore: use_build_context_synchronously final value = await showDialog( context: context, builder: (context) => MultipleChoiceDialog( title: l.settingsAboutChooseLog, current: files.first.path, options: files .map((e) => MultiChoiceOption.string( title: p.basename(e.path), option: e.path, )) .toIList(), ), ); if (value == null) return; Share.shareXFiles( [XFile(value, mimeType: 'text/plain')], subject: 'Logs from subtracks: ${String.fromCharCodes( List.generate(8, (_) => Random().nextInt(26) + 65), )}', ); }, ), ], ); } } class _MaxBitrateWifi extends HookConsumerWidget { const _MaxBitrateWifi(); @override Widget build(BuildContext context, WidgetRef ref) { final bitrate = ref.watch(settingsServiceProvider.select( (value) => value.app.maxBitrateWifi, )); final l = AppLocalizations.of(context); return _MaxBitrateOption( title: l.settingsNetworkOptionsMaxBitrateWifiTitle, bitrate: bitrate, onChange: (value) { ref.read(settingsServiceProvider.notifier).setMaxBitrateWifi(value); }, ); } } class _MaxBitrateMobile extends HookConsumerWidget { const _MaxBitrateMobile(); @override Widget build(BuildContext context, WidgetRef ref) { final bitrate = ref.watch(settingsServiceProvider.select( (value) => value.app.maxBitrateMobile, )); final l = AppLocalizations.of(context); return _MaxBitrateOption( title: l.settingsNetworkOptionsMaxBitrateMobileTitle, bitrate: bitrate, onChange: (value) { ref.read(settingsServiceProvider.notifier).setMaxBitrateMobile(value); }, ); } } class _MaxBitrateOption extends HookConsumerWidget { final String title; final int bitrate; final void Function(int value) onChange; const _MaxBitrateOption({ required this.title, required this.bitrate, required this.onChange, }); static const options = [0, 24, 32, 64, 96, 128, 192, 256, 320]; String _bitrateText(AppLocalizations l, int bitrate) { return bitrate == 0 ? l.settingsNetworkValuesUnlimitedKbps : l.settingsNetworkValuesKbps(bitrate.toString()); } @override Widget build(BuildContext context, WidgetRef ref) { final l = AppLocalizations.of(context); return ListTile( title: Text(title), subtitle: Text(_bitrateText(l, bitrate)), onTap: () async { final value = await showDialog( context: context, builder: (context) => MultipleChoiceDialog( title: title, current: bitrate, options: options .map((opt) => MultiChoiceOption.int( title: _bitrateText(l, opt), option: opt, )) .toIList(), ), ); if (value != null) { onChange(value); } }, ); } } class _StreamFormat extends HookConsumerWidget { const _StreamFormat(); static const options = ['', 'mp3', 'opus', 'ogg']; @override Widget build(BuildContext context, WidgetRef ref) { final streamFormat = ref.watch( settingsServiceProvider.select((value) => value.app.streamFormat), ); final l = AppLocalizations.of(context); return ListTile( title: Text(l.settingsNetworkOptionsStreamFormat), subtitle: Text( streamFormat ?? l.settingsNetworkOptionsStreamFormatServerDefault, ), onTap: () async { final value = await showDialog( context: context, builder: (context) => MultipleChoiceDialog( title: l.settingsNetworkOptionsStreamFormat, current: streamFormat ?? '', options: options .map((opt) => MultiChoiceOption.string( title: opt == '' ? l.settingsNetworkOptionsStreamFormatServerDefault : opt, option: opt, )) .toIList(), ), ); if (value != null) { ref .read(settingsServiceProvider.notifier) .setStreamFormat(value == '' ? null : value); } }, ); } } class _OfflineMode extends HookConsumerWidget { const _OfflineMode(); @override Widget build(BuildContext context, WidgetRef ref) { final offline = ref.watch(offlineModeProvider); final l = AppLocalizations.of(context); return SwitchListTile( value: offline, title: Text(l.settingsNetworkOptionsOfflineMode), subtitle: offline ? Text(l.settingsNetworkOptionsOfflineModeOn) : Text(l.settingsNetworkOptionsOfflineModeOff), onChanged: (value) { ref.read(offlineModeProvider.notifier).setMode(value); }, ); } } class _Sources extends HookConsumerWidget { const _Sources(); @override Widget build(BuildContext context, WidgetRef ref) { final sources = ref.watch(settingsServiceProvider.select( (value) => value.sources, )); final activeSource = ref.watch(settingsServiceProvider.select( (value) => value.activeSource, )); final l = AppLocalizations.of(context); return _Section( children: [ for (var source in sources) RadioListTile( value: source.id, groupValue: activeSource?.id, onChanged: (value) { ref .read(settingsServiceProvider.notifier) .setActiveSource(source.id); }, title: Text(source.name), subtitle: Text( source.address.toString(), maxLines: 1, softWrap: false, overflow: TextOverflow.fade, ), secondary: IconButton( icon: const Icon(Icons.edit_rounded), onPressed: () { context.pushRoute(SourceRoute(id: source.id)); }, ), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( icon: const Icon(Icons.add_rounded), label: Text(l.settingsServersActionsAdd), onPressed: () { context.pushRoute(SourceRoute()); }, ), ], ), // TODO: remove if (kDebugMode) Row( mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton.icon( icon: const Icon(Icons.add_rounded), label: const Text('Add TEST'), onPressed: () { ref .read(settingsServiceProvider.notifier) .addTestSource('TEST'); }, ), ], ), ], ); } }