mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
refactor(layering): move logic files out of screens
Search bucketing, refresh policy, folder data, history snapshot and the selection and collapsing-header mixins hold no widgets, so they move to controllers; the settings search catalog is pure presentation metadata and moves to utils. Files with many importers keep a re-export in screens.
This commit is contained in:
1 parent
ce21896ff5
commit
99ccc8826f
19 files changed
+130
-127
No files matched your search
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Expanded height of the album-style collapsing header, shared with the
|
||||
/// loading screens that show the same header before the tracks arrive.
|
||||
double collectionHeaderExpandedHeight(BuildContext context) {
|
||||
final mediaSize = MediaQuery.sizeOf(context);
|
||||
return (mediaSize.height * 0.6).clamp(400.0, 580.0);
|
||||
}
|
||||
|
||||
/// Shared collapsing-header scroll behavior for album-style screens: owns the
|
||||
/// [ScrollController] and flips [showTitleInAppBar] once the header has
|
||||
/// scrolled out of view.
|
||||
mixin CollapsingHeaderScrollMixin<T extends StatefulWidget> on State<T> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
bool showTitleInAppBar = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(_onHeaderScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(_onHeaderScroll);
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double calculateExpandedHeight(BuildContext context) =>
|
||||
collectionHeaderExpandedHeight(context);
|
||||
|
||||
void _onHeaderScroll() {
|
||||
final expandedHeight = calculateExpandedHeight(context);
|
||||
final shouldShow =
|
||||
scrollController.offset > (expandedHeight - kToolbarHeight - 20);
|
||||
if (shouldShow != showTitleInAppBar) {
|
||||
setState(() => showTitleInAppBar = shouldShow);
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+1
-1
@@ -1,5 +1,5 @@
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/controllers/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Shared multi-select state for list screens: enter/exit selection mode,
|
||||
/// toggle individual items, select all, and prune ids that fell out of the
|
||||
/// current item list. Mix into a `State<T>` alongside the screen's own
|
||||
/// per-item lookups (e.g. queue_tab, library_folder, artist screens).
|
||||
mixin SelectionModeMixin<T extends StatefulWidget> on State<T> {
|
||||
bool isSelectionMode = false;
|
||||
final Set<String> selectedIds = {};
|
||||
|
||||
void enterSelectionMode(String itemId) {
|
||||
HapticFeedback.mediumImpact();
|
||||
setState(() {
|
||||
isSelectionMode = true;
|
||||
selectedIds.add(itemId);
|
||||
});
|
||||
}
|
||||
|
||||
void exitSelectionMode() {
|
||||
setState(() {
|
||||
isSelectionMode = false;
|
||||
selectedIds.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void toggleSelection(String itemId) {
|
||||
setState(() {
|
||||
if (selectedIds.contains(itemId)) {
|
||||
selectedIds.remove(itemId);
|
||||
if (selectedIds.isEmpty) {
|
||||
isSelectionMode = false;
|
||||
}
|
||||
} else {
|
||||
selectedIds.add(itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void selectAll(Iterable<String> ids) {
|
||||
setState(() {
|
||||
selectedIds.addAll(ids);
|
||||
});
|
||||
}
|
||||
|
||||
/// Drops ids no longer present in [validIds]; call from build() with the
|
||||
/// current item ids. Exits selection mode next frame if that empties it.
|
||||
void pruneSelection(Set<String> validIds) {
|
||||
selectedIds.removeWhere((id) => !validIds.contains(id));
|
||||
if (selectedIds.isEmpty && isSelectionMode) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => isSelectionMode = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
|
||||
/// Retains derived lookups while the immutable source list is unchanged.
|
||||
/// Callers replace their list for metadata, order, or filter changes.
|
||||
class TrackHistorySnapshot {
|
||||
List<Track>? _source;
|
||||
HistoryBatchLookupRequest request = HistoryBatchLookupRequest.snapshot([]);
|
||||
|
||||
List<HistoryLookupRequest> get lookups => request.tracks;
|
||||
|
||||
void update(List<Track> tracks) {
|
||||
if (identical(tracks, _source)) return;
|
||||
_source = tracks;
|
||||
request = HistoryBatchLookupRequest.snapshot(
|
||||
tracks.map(historyLookupForTrack),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Expanded height of the album-style collapsing header, shared with the
|
||||
/// loading screens that show the same header before the tracks arrive.
|
||||
double collectionHeaderExpandedHeight(BuildContext context) {
|
||||
final mediaSize = MediaQuery.sizeOf(context);
|
||||
return (mediaSize.height * 0.6).clamp(400.0, 580.0);
|
||||
}
|
||||
|
||||
/// Shared collapsing-header scroll behavior for album-style screens: owns the
|
||||
/// [ScrollController] and flips [showTitleInAppBar] once the header has
|
||||
/// scrolled out of view.
|
||||
mixin CollapsingHeaderScrollMixin<T extends StatefulWidget> on State<T> {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
bool showTitleInAppBar = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(_onHeaderScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(_onHeaderScroll);
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double calculateExpandedHeight(BuildContext context) =>
|
||||
collectionHeaderExpandedHeight(context);
|
||||
|
||||
void _onHeaderScroll() {
|
||||
final expandedHeight = calculateExpandedHeight(context);
|
||||
final shouldShow =
|
||||
scrollController.offset > (expandedHeight - kToolbarHeight - 20);
|
||||
if (shouldShow != showTitleInAppBar) {
|
||||
setState(() => showTitleInAppBar = shouldShow);
|
||||
}
|
||||
}
|
||||
}
|
||||
export 'package:spotiflac_android/controllers/collapsing_header_scroll_mixin.dart';
|
||||
@@ -54,7 +54,7 @@ import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
import 'package:spotiflac_android/screens/album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/artist_screen.dart';
|
||||
import 'package:spotiflac_android/screens/extension_collection_screens.dart';
|
||||
import 'package:spotiflac_android/screens/home_search_logic.dart';
|
||||
import 'package:spotiflac_android/controllers/home_search_logic.dart';
|
||||
import 'package:spotiflac_android/services/csv_import_service.dart';
|
||||
import 'package:spotiflac_android/services/cover_download_service.dart';
|
||||
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
|
||||
|
||||
@@ -35,7 +35,7 @@ import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
import 'package:spotiflac_android/utils/cover_art_utils.dart';
|
||||
import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart';
|
||||
import 'package:spotiflac_android/screens/selection_mode_mixin.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_data.dart';
|
||||
import 'package:spotiflac_android/controllers/library_tracks_folder_data.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
|
||||
|
||||
@@ -80,7 +80,7 @@ import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
||||
import 'package:spotiflac_android/screens/local_album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/mornye_library_screen.dart';
|
||||
import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/controllers/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
import 'package:spotiflac_android/widgets/download_service_picker.dart';
|
||||
|
||||
@@ -1,56 +1 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Shared multi-select state for list screens: enter/exit selection mode,
|
||||
/// toggle individual items, select all, and prune ids that fell out of the
|
||||
/// current item list. Mix into a `State<T>` alongside the screen's own
|
||||
/// per-item lookups (e.g. queue_tab, library_folder, artist screens).
|
||||
mixin SelectionModeMixin<T extends StatefulWidget> on State<T> {
|
||||
bool isSelectionMode = false;
|
||||
final Set<String> selectedIds = {};
|
||||
|
||||
void enterSelectionMode(String itemId) {
|
||||
HapticFeedback.mediumImpact();
|
||||
setState(() {
|
||||
isSelectionMode = true;
|
||||
selectedIds.add(itemId);
|
||||
});
|
||||
}
|
||||
|
||||
void exitSelectionMode() {
|
||||
setState(() {
|
||||
isSelectionMode = false;
|
||||
selectedIds.clear();
|
||||
});
|
||||
}
|
||||
|
||||
void toggleSelection(String itemId) {
|
||||
setState(() {
|
||||
if (selectedIds.contains(itemId)) {
|
||||
selectedIds.remove(itemId);
|
||||
if (selectedIds.isEmpty) {
|
||||
isSelectionMode = false;
|
||||
}
|
||||
} else {
|
||||
selectedIds.add(itemId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void selectAll(Iterable<String> ids) {
|
||||
setState(() {
|
||||
selectedIds.addAll(ids);
|
||||
});
|
||||
}
|
||||
|
||||
/// Drops ids no longer present in [validIds]; call from build() with the
|
||||
/// current item ids. Exits selection mode next frame if that empties it.
|
||||
void pruneSelection(Set<String> validIds) {
|
||||
selectedIds.removeWhere((id) => !validIds.contains(id));
|
||||
if (selectedIds.isEmpty && isSelectionMode) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => isSelectionMode = false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export 'package:spotiflac_android/controllers/selection_mode_mixin.dart';
|
||||
@@ -23,7 +23,7 @@ import 'package:spotiflac_android/screens/settings/log_screen.dart';
|
||||
import 'package:spotiflac_android/screens/settings/lyrics_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/metadata_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/playback_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/utils/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/services/app_remote_config_service.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
|
||||
@@ -1,20 +1 @@
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
|
||||
/// Retains derived lookups while the immutable source list is unchanged.
|
||||
/// Callers replace their list for metadata, order, or filter changes.
|
||||
class TrackHistorySnapshot {
|
||||
List<Track>? _source;
|
||||
HistoryBatchLookupRequest request = HistoryBatchLookupRequest.snapshot([]);
|
||||
|
||||
List<HistoryLookupRequest> get lookups => request.tracks;
|
||||
|
||||
void update(List<Track> tracks) {
|
||||
if (identical(tracks, _source)) return;
|
||||
_source = tracks;
|
||||
request = HistoryBatchLookupRequest.snapshot(
|
||||
tracks.map(historyLookupForTrack),
|
||||
);
|
||||
}
|
||||
}
|
||||
export 'package:spotiflac_android/controllers/track_history_snapshot.dart';
|
||||
File renamed without changes.
@@ -9,7 +9,7 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/models/theme_settings.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/playback_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/utils/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/theme/app_theme.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:collection';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_data.dart';
|
||||
import 'package:spotiflac_android/controllers/library_tracks_folder_data.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:spotiflac_android/models/download_item.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_state.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/screens/home_search_logic.dart';
|
||||
import 'package:spotiflac_android/controllers/home_search_logic.dart';
|
||||
import 'package:spotiflac_android/services/audio_metadata_mapper.dart';
|
||||
import 'package:spotiflac_android/services/ffmpeg_models.dart';
|
||||
import 'package:spotiflac_android/services/id3v23_lyrics.dart';
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/models/theme_settings.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/settings/playback_settings_page.dart';
|
||||
import 'package:spotiflac_android/screens/settings/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/utils/settings_search_catalog.dart';
|
||||
import 'package:spotiflac_android/theme/app_theme.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/controllers/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
Reference in new issue
Block a user