mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 21:02:09 +02:00
Read embedded lyrics through the playback metadata fallback when the lyrics reader misses them. Restore edge icon highlighting during navbar drags, tune selection contrast, and blend the profile name field into its card.
186 lines
6.7 KiB
Dart
186 lines
6.7 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/providers/user_profile_provider.dart';
|
|
import 'package:spotiflac_android/services/user_profile_store.dart';
|
|
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
|
import 'package:spotiflac_android/widgets/app_action_button.dart';
|
|
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
|
import 'package:spotiflac_android/widgets/profile_avatar.dart';
|
|
import 'package:spotiflac_android/widgets/settings_group.dart';
|
|
|
|
class ProfileSettingsPage extends ConsumerStatefulWidget {
|
|
const ProfileSettingsPage({super.key, required this.profile});
|
|
|
|
final UserProfile profile;
|
|
|
|
@override
|
|
ConsumerState<ProfileSettingsPage> createState() =>
|
|
_ProfileSettingsPageState();
|
|
}
|
|
|
|
class _ProfileSettingsPageState extends ConsumerState<ProfileSettingsPage> {
|
|
late final _name = TextEditingController(text: widget.profile.name);
|
|
Uint8List? _photo;
|
|
bool _removePhoto = false;
|
|
bool _busy = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_name.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _pickPhoto() async {
|
|
setState(() => _busy = true);
|
|
try {
|
|
final picked = await FilePicker.pickFile(
|
|
type: FileType.image,
|
|
darwinOptions: const DarwinOptions(
|
|
assetRepresentationMode: DarwinAssetRepresentationMode.compatible,
|
|
),
|
|
);
|
|
if (picked == null) return;
|
|
if ((await picked.length() ?? 0) > 20 * 1024 * 1024) {
|
|
throw const FormatException('Photo too large');
|
|
}
|
|
final bytes = await picked.readAsBytes();
|
|
final photo = await prepareProfilePhoto(bytes);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_photo = photo;
|
|
_removePhoto = false;
|
|
});
|
|
} catch (_) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(context.l10n.profilePhotoError)));
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _save() async {
|
|
FocusManager.instance.primaryFocus?.unfocus();
|
|
setState(() => _busy = true);
|
|
try {
|
|
await ref
|
|
.read(userProfileProvider.notifier)
|
|
.save(name: _name.text, photo: _photo, removePhoto: _removePhoto);
|
|
if (mounted) Navigator.of(context).pop();
|
|
} catch (_) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text(context.l10n.profileSaveError)));
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final inset = wideListInset(context);
|
|
final hasPhoto =
|
|
_photo != null || (!_removePhoto && widget.profile.photoPath != null);
|
|
return PopScope(
|
|
canPop: !_busy,
|
|
child: Scaffold(
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
AppSliverHeader.page(title: context.l10n.profileTitle),
|
|
SliverPadding(
|
|
padding: EdgeInsets.fromLTRB(24 + inset, 16, 24 + inset, 32),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Center(
|
|
child: ValueListenableBuilder<TextEditingValue>(
|
|
valueListenable: _name,
|
|
builder: (context, value, _) => ProfileAvatar(
|
|
name: value.text,
|
|
photo: _photo,
|
|
photoPath: _removePhoto
|
|
? null
|
|
: widget.profile.photoPath,
|
|
size: 128,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Center(
|
|
child: AppActionButton(
|
|
onPressed: _busy ? null : _pickPhoto,
|
|
icon: const Icon(Icons.photo_library_outlined),
|
|
label: Text(context.l10n.profileChangePhoto),
|
|
outlined: true,
|
|
tonal: true,
|
|
),
|
|
),
|
|
if (hasPhoto)
|
|
Center(
|
|
child: TextButton(
|
|
onPressed: _busy
|
|
? null
|
|
: () => setState(() {
|
|
_photo = null;
|
|
_removePhoto = true;
|
|
}),
|
|
child: Text(context.l10n.profileRemovePhoto),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
SettingsGroup(
|
|
margin: EdgeInsets.zero,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: TextField(
|
|
controller: _name,
|
|
enabled: !_busy,
|
|
maxLength: 50,
|
|
textCapitalization: TextCapitalization.words,
|
|
textInputAction: TextInputAction.done,
|
|
onSubmitted: (_) => _busy ? null : _save(),
|
|
decoration: InputDecoration(
|
|
labelText: context.l10n.profileName,
|
|
hintText: context.l10n.profileNameHint,
|
|
filled: false,
|
|
contentPadding: EdgeInsets.zero,
|
|
border: InputBorder.none,
|
|
counterText: '',
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
AppActionButton(
|
|
onPressed: _busy ? null : _save,
|
|
icon: _busy
|
|
? const SizedBox.square(
|
|
dimension: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.check),
|
|
label: Text(context.l10n.dialogSave),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(height: MediaQuery.paddingOf(context).bottom),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|