mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
feat(profile): add local profiles with backup and restore
Move Mornye Settings to the Home avatar and keep search above the profile card. Center the metadata artwork and actions, and make artist names clickable in the Material player.
This commit is contained in:
1 parent
5b519070a3
commit
d8d46108bc
19 files changed
+1003
-80
No files matched your search
@@ -52,6 +52,7 @@ import 'package:spotiflac_android/widgets/in_library_badge.dart';
|
||||
import 'package:spotiflac_android/widgets/preview_button.dart';
|
||||
import 'package:spotiflac_android/widgets/app_search_field.dart';
|
||||
import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart';
|
||||
import 'package:spotiflac_android/widgets/profile_avatar.dart';
|
||||
import 'package:spotiflac_android/widgets/downloadable_cover.dart';
|
||||
|
||||
part 'home_tab_helpers.dart';
|
||||
@@ -896,6 +897,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
title: widget.mode == HomeTabMode.search
|
||||
? context.l10n.mornyeSearch
|
||||
: context.l10n.homeTitle,
|
||||
actions: context.isMornye && _showsHome
|
||||
? const [HomeProfileButton()]
|
||||
: null,
|
||||
),
|
||||
|
||||
SliverToBoxAdapter(
|
||||
|
||||
+31
-10
@@ -494,12 +494,16 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
}
|
||||
|
||||
void _onShellTabRequested(ShellTab tab) {
|
||||
if (tab == ShellTab.settings && context.isMornye) {
|
||||
unawaited(_openProfileSettings());
|
||||
return;
|
||||
}
|
||||
final showStore = ref.read(
|
||||
settingsProvider.select((s) => s.showExtensionStore),
|
||||
);
|
||||
final index = switch (tab) {
|
||||
ShellTab.home => 0,
|
||||
ShellTab.search => context.isMornye ? (showStore ? 4 : 3) : 0,
|
||||
ShellTab.search => context.isMornye ? (showStore ? 3 : 2) : 0,
|
||||
ShellTab.library => 1,
|
||||
ShellTab.repository => showStore ? 2 : null,
|
||||
ShellTab.settings => showStore ? 3 : 2,
|
||||
@@ -507,6 +511,24 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
if (index != null) _onNavTap(index, resetHome: tab != ShellTab.search);
|
||||
}
|
||||
|
||||
bool _settingsPageOpen = false;
|
||||
|
||||
Future<void> _openProfileSettings() async {
|
||||
if (_settingsPageOpen) return;
|
||||
_settingsPageOpen = true;
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
ref.read(previewPlayerProvider.notifier).stop();
|
||||
try {
|
||||
await Navigator.of(context).push(
|
||||
slidePageRoute<void>(
|
||||
page: const Scaffold(body: SettingsTab(asPage: true)),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_settingsPageOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _onNavTap(int index, {bool resetHome = true}) {
|
||||
final showStore = ref.read(
|
||||
settingsProvider.select((s) => s.showExtensionStore),
|
||||
@@ -594,7 +616,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
|
||||
final trackState = ref.read(trackProvider);
|
||||
final isSearchTab =
|
||||
_currentIndex == (context.isMornye ? (showStore ? 4 : 3) : 0);
|
||||
_currentIndex == (context.isMornye ? (showStore ? 3 : 2) : 0);
|
||||
|
||||
final isKeyboardVisible = MediaQuery.viewInsetsOf(context).bottom > 0;
|
||||
|
||||
@@ -664,7 +686,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
if (index == 0) return _homeTabNavigatorKey.currentState;
|
||||
if (index == 1) return _libraryTabNavigatorKey.currentState;
|
||||
if (showStore && index == 2) return _repoTabNavigatorKey.currentState;
|
||||
if (context.isMornye && index == (showStore ? 4 : 3)) {
|
||||
if (context.isMornye && index == (showStore ? 3 : 2)) {
|
||||
return _searchTabNavigatorKey.currentState;
|
||||
}
|
||||
return null;
|
||||
@@ -732,7 +754,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
heroAnimationsEnabled: heroAnimationsEnabled,
|
||||
child: const RepoTab(),
|
||||
),
|
||||
const SettingsTab(),
|
||||
if (!context.isMornye) const SettingsTab(),
|
||||
if (context.isMornye)
|
||||
_TabNavigator(
|
||||
key: const ValueKey('tab-search'),
|
||||
@@ -803,13 +825,12 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
),
|
||||
label: l10n.navStore,
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(
|
||||
context.isMornye ? CupertinoIcons.gear : Icons.settings_outlined,
|
||||
if (!context.isMornye)
|
||||
NavigationDestination(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
selectedIcon: SpinIcon(child: const Icon(Icons.settings)),
|
||||
label: l10n.navSettings,
|
||||
),
|
||||
selectedIcon: SpinIcon(child: const Icon(Icons.settings)),
|
||||
label: l10n.navSettings,
|
||||
),
|
||||
if (context.isMornye)
|
||||
NavigationDestination(
|
||||
icon: const Icon(CupertinoIcons.search),
|
||||
|
||||
@@ -1262,14 +1262,22 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
mediaItem.artist ?? '',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final track = ref
|
||||
.watch(playerCollectionTrackProvider(mediaItem))
|
||||
.value;
|
||||
return ClickableArtistName(
|
||||
artistName: mediaItem.artist ?? '',
|
||||
artistId: track?.artistId,
|
||||
extensionId: track?.source,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/user_profile_provider.dart';
|
||||
import 'package:spotiflac_android/services/backup_service.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
@@ -65,6 +66,9 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
|
||||
final file = await BackupService.writeBackupArchive(
|
||||
settings: settings,
|
||||
profile: _includeSettings
|
||||
? await ref.read(userProfileProvider.future)
|
||||
: null,
|
||||
includeHistory: _includeHistory,
|
||||
loadHistoryPage: (limit, offset) =>
|
||||
HistoryDatabase.instance.getAll(limit: limit, offset: offset),
|
||||
@@ -125,6 +129,11 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
|
||||
setState(() => _isImporting = true);
|
||||
try {
|
||||
if (bundle.profile != null) {
|
||||
await ref
|
||||
.read(userProfileProvider.notifier)
|
||||
.restoreFromBackup(bundle.profile!);
|
||||
}
|
||||
if (bundle.hasSettings) {
|
||||
await ref
|
||||
.read(settingsProvider.notifier)
|
||||
@@ -201,6 +210,11 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
icon: Icons.settings_outlined,
|
||||
label: l10n.backupContentsSettings,
|
||||
),
|
||||
if (bundle.profile != null)
|
||||
_ContentRow(
|
||||
icon: Icons.person_outline,
|
||||
label: l10n.profileTitle,
|
||||
),
|
||||
if (bundle.hasHistory)
|
||||
_ContentRow(
|
||||
icon: Icons.history,
|
||||
@@ -281,6 +295,7 @@ class _BackupRestorePageState extends ConsumerState<BackupRestorePage> {
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.settings_outlined,
|
||||
title: l10n.backupContentsSettings,
|
||||
subtitle: l10n.backupIncludesProfile,
|
||||
value: _includeSettings,
|
||||
onChanged: _isBusy
|
||||
? null
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
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,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/user_profile_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/profile_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/about_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/app_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/appearance_settings_page.dart';
|
||||
@@ -24,6 +26,7 @@ import 'package:spotiflac_android/widgets/animation_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/app_search_field.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
import 'package:spotiflac_android/widgets/profile_avatar.dart';
|
||||
|
||||
/// One entry on the Settings tab.
|
||||
class _Destination {
|
||||
@@ -66,7 +69,9 @@ class _Group {
|
||||
}
|
||||
|
||||
class SettingsTab extends ConsumerStatefulWidget {
|
||||
const SettingsTab({super.key});
|
||||
const SettingsTab({super.key, this.asPage = false});
|
||||
|
||||
final bool asPage;
|
||||
|
||||
@override
|
||||
ConsumerState<SettingsTab> createState() => _SettingsTabState();
|
||||
@@ -389,7 +394,10 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
return MornyeSettingsTheme(
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
AppSliverHeader.tabRoot(title: context.l10n.settingsTitle),
|
||||
if (widget.asPage)
|
||||
AppSliverHeader.page(title: context.l10n.settingsTitle)
|
||||
else
|
||||
AppSliverHeader.tabRoot(title: context.l10n.settingsTitle),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
@@ -408,6 +416,45 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final profile = ref.watch(userProfileProvider).value;
|
||||
return SettingsGroup(
|
||||
margin: margin,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
leading: ProfileAvatar(
|
||||
name: profile?.name ?? '',
|
||||
photoPath: profile?.photoPath,
|
||||
size: 64,
|
||||
),
|
||||
title: Text(
|
||||
profile?.name.isNotEmpty == true
|
||||
? profile!.name
|
||||
: context.l10n.profileSetUp,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
subtitle: Text(context.l10n.profileEdit),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: profile == null
|
||||
? null
|
||||
: () => _navigateTo(
|
||||
context,
|
||||
ProfileSettingsPage(profile: profile),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
...body,
|
||||
const NavBarSliverSpacer(),
|
||||
const SliverFillRemaining(hasScrollBody: false, child: SizedBox()),
|
||||
|
||||
@@ -77,9 +77,10 @@ extension _TrackMetadataCards on _TrackMetadataScreenState {
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
_buildActionButtons(context, ref, colorScheme, _fileExists),
|
||||
if (!context.isMornye) ...[
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(context, ref, colorScheme, _fileExists),
|
||||
],
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
@@ -58,57 +58,56 @@ extension _TrackMetadataMornye on _TrackMetadataScreenState {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Hero(
|
||||
tag: _coverHeroTag,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: SizedBox.square(
|
||||
dimension: 70,
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Hero(
|
||||
tag: _coverHeroTag,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: PlayerArtwork(
|
||||
artUri: artwork,
|
||||
colorScheme: scheme,
|
||||
iconSize: 80,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ExplicitTrackTitle(
|
||||
title: trackName,
|
||||
explicit: isExplicit,
|
||||
style: theme.textTheme.titleLarge,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
artistName,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (albumName.trim().isNotEmpty)
|
||||
Text(
|
||||
albumName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ExplicitTrackTitle(
|
||||
title: trackName,
|
||||
explicit: isExplicit,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
artistName,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (albumName.trim().isNotEmpty)
|
||||
Text(
|
||||
albumName,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildActionButtons(context, ref, scheme, _fileExists),
|
||||
if (_hasCheckedFile && !_fileExists)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
|
||||
Reference in new issue
Block a user