spotube/lib/pages/library/user_local_tracks/local_folder.dart
Alessio fece073def This pull request primarily involves the removal of several configuration files and assets, as well as minor updates to documentation. The most significant changes are the deletion of various .vscode configuration files and the removal of unused assets from the project.
Configuration File Removals:

    .vscode/c_cpp_properties.json: Removed the entire configuration for C/C++ properties.
    .vscode/launch.json: Removed the Dart launch configurations for different environments and modes.
    .vscode/settings.json: Removed settings related to CMake, spell checking, file nesting, and Dart Flutter SDK path.
    .vscode/snippets.code-snippets: Removed code snippets for Dart, including PaginatedState and PaginatedNotifier templates.
    .vscode/tasks.json: Removed the tasks configuration file.

Documentation Updates:

    CONTRIBUTION.md: Removed heart emoji from the introductory text.
    README.md: Updated the logo image and made minor text adjustments, including removing emojis and updating section titles. [1] [2] [3] [4] [5]

Asset Removals:

    lib/collections/assets.gen.dart: Removed multiple unused asset references, including images related to Spotube logos and banners. [1] [2] [3]

Minor Code Cleanups:

    cli/commands/build/linux.dart, cli/commands/build/windows.dart, cli/commands/translated.dart, cli/commands/untranslated.dart: Adjusted import statements for consistency. [1] [2] [3] [4]
    integration_test/app_test.dart: Removed an unnecessary blank line.
    lib/collections/routes.dart: Commented out the TrackRoute configuration.
2025-04-13 18:40:37 +02:00

414 lines
18 KiB
Dart

import 'dart:io';
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart' as material;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/expandable_search/expandable_search.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/track_presentation/sort_tracks_dropdown.dart';
import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/string.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/models/local_track.dart';
import 'package:spotube/modules/library/local_folder/cache_export_dialog.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/local_tracks/local_tracks_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/service_utils.dart';
@RoutePage()
class LocalLibraryPage extends HookConsumerWidget {
static const name = "local_library_page";
final String location;
final bool isDownloads;
final bool isCache;
const LocalLibraryPage(
this.location, {
super.key,
this.isDownloads = false,
this.isCache = false,
});
Future<void> playLocalTracks(
WidgetRef ref,
List<LocalTrack> tracks, {
LocalTrack? currentTrack,
}) async {
final playlist = ref.read(audioPlayerProvider);
final playback = ref.read(audioPlayerProvider.notifier);
currentTrack ??= tracks.first;
final isPlaylistPlaying = playlist.containsTracks(tracks);
if (!isPlaylistPlaying) {
var indexWhere = tracks.indexWhere((s) => s.id == currentTrack?.id);
await playback.load(
tracks,
initialIndex: indexWhere,
autoPlay: true,
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist.activeTrack?.id) {
await playback.jumpToTrack(currentTrack);
}
}
@override
Widget build(BuildContext context, ref) {
final scale = context.theme.scaling;
final sortBy = useState<SortBy>(SortBy.none);
final playlist = ref.watch(audioPlayerProvider);
final trackSnapshot = ref.watch(localTracksProvider);
final isPlaylistPlaying = playlist.containsTracks(
trackSnapshot.asData?.value.values.flattened.toList() ?? []);
final searchController = useShadcnTextEditingController();
useValueListenable(searchController);
final searchFocus = useFocusNode();
final isFiltering = useState(false);
final controller = useScrollController();
final directorySize = useMemoized(() async {
final dir = Directory(location);
final files = await dir.list(recursive: true).toList();
final filesLength =
await Future.wait(files.whereType<File>().map((e) => e.length()));
return (filesLength.sum.toInt() / pow(10, 9)).toStringAsFixed(2);
}, [location]);
return SafeArea(
bottom: false,
child: Scaffold(
headers: [
TitleBar(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 0,
),
surfaceBlur: 0,
leading: const [BackButton()],
title: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isDownloads
? context.l10n.downloads
: isCache
? context.l10n.cache_folder.capitalize()
: location,
),
FutureBuilder<String>(
future: directorySize,
builder: (context, snapshot) {
return Text(
"${(snapshot.data ?? 0)} GB",
).xSmall().muted();
},
)
],
),
backgroundColor: Colors.transparent,
trailingGap: 10,
trailing: [
if (isCache) ...[
IconButton.outline(
size: ButtonSize.small,
icon: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.delete),
Text(context.l10n.clear_cache)
],
).xSmall().iconSmall(),
onPressed: () async {
final accepted = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.clear_cache_confirmation),
actions: [
Button.outline(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(context.l10n.decline),
),
Button.destructive(
onPressed: () async {
Navigator.of(context).pop(true);
},
child: Text(context.l10n.accept),
),
],
),
);
if (accepted ?? false) return;
final cacheDir = Directory(
await UserPreferencesNotifier.getMusicCacheDir(),
);
if (cacheDir.existsSync()) {
await cacheDir.delete(recursive: true);
}
},
),
IconButton.outline(
size: ButtonSize.small,
icon: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(SpotubeIcons.export),
Text(
context.l10n.export,
)
],
).xSmall().iconSmall(),
onPressed: () async {
final exportPath =
await FilePicker.platform.getDirectoryPath();
if (exportPath == null) return;
final exportDirectory = Directory(exportPath);
if (!exportDirectory.existsSync()) {
await exportDirectory.create(recursive: true);
}
final cacheDir = Directory(
await UserPreferencesNotifier.getMusicCacheDir());
if (!context.mounted) return;
await showDialog(
context: context,
builder: (context) {
return LocalFolderCacheExportDialog(
cacheDir: cacheDir,
exportDir: exportDirectory,
);
},
);
},
),
]
],
),
],
child: LayoutBuilder(
builder: (context, constraints) => Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
const Gap(5),
Button.primary(
onPressed: trackSnapshot.asData?.value != null
? () async {
if (trackSnapshot
.asData?.value.isNotEmpty ==
true) {
if (!isPlaylistPlaying) {
await playLocalTracks(
ref,
trackSnapshot
.asData!.value[location] ??
[],
);
}
}
}
: null,
leading: Icon(
isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
),
child: Text(context.l10n.play),
),
const Spacer(),
if (constraints.smAndDown)
ExpandableSearchButton(
isFiltering: isFiltering.value,
onPressed: (value) => isFiltering.value = value,
searchFocus: searchFocus,
)
else
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 300 * scale,
maxHeight: 38 * scale,
),
child: ExpandableSearchField(
isFiltering: true,
onChangeFiltering: (value) {},
searchController: searchController,
searchFocus: searchFocus,
),
),
const Gap(5),
SortTracksDropdown(
value: sortBy.value,
onChanged: (value) {
sortBy.value = value;
},
),
const Gap(5),
IconButton.outline(
icon: const Icon(SpotubeIcons.refresh),
onPressed: () {
ref.invalidate(localTracksProvider);
},
)
],
),
),
ExpandableSearchField(
searchController: searchController,
searchFocus: searchFocus,
isFiltering: isFiltering.value,
onChangeFiltering: (value) => isFiltering.value = value,
),
HookBuilder(builder: (context) {
return trackSnapshot.when(
data: (tracks) {
final sortedTracks = useMemoized(() {
return ServiceUtils.sortTracks(
tracks[location] ?? <LocalTrack>[],
sortBy.value);
}, [sortBy.value, tracks]);
final filteredTracks = useMemoized(() {
if (searchController.text.isEmpty) {
return sortedTracks;
}
return sortedTracks
.map((e) => (
weightedRatio(
"${e.name} - ${e.artists?.asString() ?? ""}",
searchController.text,
),
e,
))
.toList()
.sorted(
(a, b) => b.$1.compareTo(a.$1),
)
.where((e) => e.$1 > 50)
.map((e) => e.$2)
.toList()
.toList();
}, [searchController.text, sortedTracks]);
if (!trackSnapshot.isLoading &&
filteredTracks.isEmpty) {
return Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Undraw(
illustration: UndrawIllustration.empty,
height: 200 * scale,
color: context.theme.colorScheme.primary,
),
const Gap(10),
Text(
context.l10n.nothing_found,
textAlign: TextAlign.center,
).muted().small()
],
),
);
}
return Expanded(
child: material.RefreshIndicator.adaptive(
onRefresh: () async {
ref.invalidate(localTracksProvider);
},
child: InterScrollbar(
controller: controller,
child: Skeletonizer(
enabled: trackSnapshot.isLoading,
child: ListView.builder(
controller: controller,
physics:
const AlwaysScrollableScrollPhysics(),
itemCount: trackSnapshot.isLoading
? 5
: filteredTracks.length,
itemBuilder: (context, index) {
if (trackSnapshot.isLoading) {
return TrackTile(
playlist: playlist,
track: FakeData.track,
index: index,
);
}
final track = filteredTracks[index];
return TrackTile(
index: index,
playlist: playlist,
track: track,
userPlaylist: false,
onTap: () async {
await playLocalTracks(
ref,
sortedTracks,
currentTrack: track,
);
},
);
},
),
),
),
),
);
},
loading: () => Expanded(
child: Skeletonizer(
enabled: true,
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => TrackTile(
track: FakeData.track,
index: index,
playlist: playlist,
),
),
),
),
error: (error, stackTrace) =>
Text(error.toString() + stackTrace.toString()),
);
})
],
))),
);
}
}