subtracks/lib/app/pages/source_page.dart
austinried 963c15636c aab build, release signing
clean up analyze issues
2023-04-28 12:26:02 +09:00

259 lines
8.2 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../database/database.dart';
import '../../models/settings.dart';
import '../../services/settings_service.dart';
import '../items.dart';
class SourcePage extends HookConsumerWidget {
final int? id;
const SourcePage({
super.key,
@pathParam this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final source = ref.watch(settingsServiceProvider.select(
(value) => value.sources.singleWhereOrNull((e) => e.id == id)
as SubsonicSettings?,
));
final form = useState(GlobalKey<FormState>()).value;
final theme = Theme.of(context);
final l = AppLocalizations.of(context);
final isSaving = useState(false);
final isDeleting = useState(false);
final name = LabeledTextField(
label: l.settingsServersFieldsName,
initialValue: source?.name,
required: true,
);
final address = LabeledTextField(
label: l.settingsServersFieldsAddress,
initialValue: source?.address.toString(),
keyboardType: TextInputType.url,
required: true,
validator: (value, label) {
if (Uri.tryParse(value!) == null) {
return '$label must be a valid URL';
}
return null;
},
);
final username = LabeledTextField(
label: l.settingsServersFieldsUsername,
initialValue: source?.username,
required: true,
);
final password = LabeledTextField(
label: l.settingsServersFieldsPassword,
initialValue: source?.password,
obscureText: true,
required: true,
);
return WillPopScope(
onWillPop: () async => !isSaving.value && !isDeleting.value,
child: Scaffold(
appBar: AppBar(),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (source != null && source.isActive != true)
FloatingActionButton(
backgroundColor: theme.colorScheme.tertiaryContainer,
foregroundColor: theme.colorScheme.onTertiaryContainer,
onPressed: !isSaving.value && !isDeleting.value
? () async {
final router = context.router;
try {
isDeleting.value = true;
await ref
.read(settingsServiceProvider.notifier)
.deleteSource(source.id);
} finally {
isDeleting.value = false;
}
router.pop();
}
: null,
child: isDeleting.value
? SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: theme.colorScheme.onTertiaryContainer,
),
)
: const Icon(Icons.delete_forever_rounded),
),
const SizedBox(width: 12),
FloatingActionButton.extended(
heroTag: null,
icon: isSaving.value
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(),
)
: const Icon(Icons.save_rounded),
label: Text(l.settingsServersActionsSave),
onPressed: !isSaving.value && !isDeleting.value
? () async {
final router = context.router;
if (!form.currentState!.validate()) {
return;
}
var error = false;
try {
isSaving.value = true;
if (source != null) {
await ref
.read(settingsServiceProvider.notifier)
.updateSource(
source.copyWith(
name: name.value,
address: Uri.parse(address.value),
username: username.value,
password: password.value,
),
);
} else {
await ref
.read(settingsServiceProvider.notifier)
.createSource(
SourcesCompanion.insert(
name: name.value,
address: Uri.parse(address.value),
),
SubsonicSourcesCompanion.insert(
features: IList(),
username: username.value,
password: password.value,
useTokenAuth: const Value(true),
),
);
}
} catch (err) {
// TOOD: toast the error or whatever
error = true;
} finally {
isSaving.value = false;
}
if (!error) {
router.pop();
}
}
: null,
),
],
),
body: Form(
key: form,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ListView(
children: [
const SizedBox(height: 96 - kToolbarHeight),
Text(
source == null
? l.settingsServersActionsAdd
: l.settingsServersActionsEdit,
style: theme.textTheme.displaySmall,
),
name,
address,
username,
password,
const FabPadding(),
],
),
),
),
),
);
}
}
class LabeledTextField extends HookConsumerWidget {
final String label;
final String? initialValue;
final bool obscureText;
final bool required;
final TextInputType? keyboardType;
final String? Function(String? value, String label)? validator;
// ignore: prefer_const_constructors_in_immutables
LabeledTextField({
super.key,
required this.label,
this.initialValue,
this.obscureText = false,
this.keyboardType,
this.validator,
this.required = false,
});
late final TextEditingController _controller;
String get value => _controller.text;
String? _requiredValidator(String? value) {
if (value == null || value.isEmpty) {
return '$label is required';
}
return null;
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
_controller = useTextEditingController(text: initialValue);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
Text(
label,
style: theme.textTheme.titleMedium,
),
TextFormField(
controller: _controller,
obscureText: obscureText,
keyboardType: keyboardType,
validator: (value) {
String? error;
if (required) {
error = _requiredValidator(value);
if (error != null) {
return error;
}
}
if (validator != null) {
return validator!(value, label);
}
return null;
},
),
],
);
}
}