subtracks/lib/app/pages/settings_page.dart
2023-05-14 14:29:04 +09:00

449 lines
12 KiB
Dart

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<Widget> 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<String>(
context: context,
builder: (context) => MultipleChoiceDialog<String>(
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<int>(
context: context,
builder: (context) => MultipleChoiceDialog<int>(
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<String>(
context: context,
builder: (context) => MultipleChoiceDialog<String>(
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<int>(
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');
},
),
],
),
],
);
}
}