mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-20 23:24:52 +02:00
fix: unify search bar, filter chips, tab navigation, and clean up comments
This commit is contained in:
@@ -1181,7 +1181,7 @@ func (c *DeezerClient) getJSON(ctx context.Context, endpoint string, dst interfa
|
||||
|
||||
for attempt := 0; attempt <= deezerMaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
delay := deezerRetryDelay * time.Duration(1<<(attempt-1)) // Exponential backoff
|
||||
delay := deezerRetryDelay * time.Duration(1<<(attempt-1))
|
||||
GoLog("[Deezer] Retry %d/%d after %v...\n", attempt, deezerMaxRetries, delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
@@ -1194,7 +1194,6 @@ func (c *DeezerClient) getJSON(ctx context.Context, endpoint string, dst interfa
|
||||
lastErr = err
|
||||
errStr := err.Error()
|
||||
|
||||
// Check if error is retryable
|
||||
isRetryable := strings.Contains(errStr, "timeout") ||
|
||||
strings.Contains(errStr, "connection reset") ||
|
||||
strings.Contains(errStr, "connection refused") ||
|
||||
|
||||
@@ -48,7 +48,6 @@ func CheckAvailability(spotifyID, isrc string) (string, error) {
|
||||
}
|
||||
|
||||
// SetSongLinkNetworkOptions is kept for backward compatibility.
|
||||
// It now applies global network compatibility options for all backend API requests.
|
||||
func SetSongLinkNetworkOptions(allowHTTP, insecureTLS bool) {
|
||||
SetNetworkCompatibilityOptions(allowHTTP, insecureTLS)
|
||||
}
|
||||
@@ -910,7 +909,6 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
// MP3/Opus: return metadata for Dart-side FFmpeg embedding
|
||||
resp := map[string]any{
|
||||
"success": true,
|
||||
"method": "ffmpeg",
|
||||
|
||||
@@ -300,14 +300,11 @@ func DoRequestWithRetry(client *http.Client, req *http.Request, config RetryConf
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for ISP blocking via HTTP status codes
|
||||
// Some ISPs return 403 or 451 when blocking content
|
||||
if resp.StatusCode == 403 || resp.StatusCode == 451 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
bodyStr := strings.ToLower(string(body))
|
||||
|
||||
// Check if response looks like ISP blocking page
|
||||
ispBlockingIndicators := []string{
|
||||
"blocked", "forbidden", "access denied", "not available in your",
|
||||
"restricted", "censored", "unavailable for legal", "blocked by",
|
||||
@@ -518,7 +515,6 @@ func IsISPBlocking(err error, requestURL string) *ISPBlockingError {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns true if ISP blocking was detected
|
||||
func CheckAndLogISPBlocking(err error, requestURL string, tag string) bool {
|
||||
ispErr := IsISPBlocking(err, requestURL)
|
||||
if ispErr != nil {
|
||||
@@ -553,7 +549,6 @@ func extractDomain(rawURL string) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// If ISP blocking is detected, returns a more descriptive error
|
||||
func WrapErrorWithISPCheck(err error, requestURL string, tag string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
|
||||
@@ -83,7 +83,6 @@ func SetLyricsProviderOrder(providers []string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate provider names
|
||||
validNames := map[string]bool{
|
||||
LyricsProviderSpotifyAPI: true,
|
||||
LyricsProviderLRCLIB: true,
|
||||
@@ -105,7 +104,6 @@ func SetLyricsProviderOrder(providers []string) {
|
||||
GoLog("[Lyrics] Provider order set to: %v\n", valid)
|
||||
}
|
||||
|
||||
// GetLyricsProviderOrder returns the current lyrics provider order.
|
||||
func GetLyricsProviderOrder() []string {
|
||||
lyricsProvidersMu.RLock()
|
||||
defer lyricsProvidersMu.RUnlock()
|
||||
@@ -119,7 +117,6 @@ func GetLyricsProviderOrder() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAvailableLyricsProviders returns metadata about all available providers.
|
||||
func GetAvailableLyricsProviders() []map[string]interface{} {
|
||||
return []map[string]interface{}{
|
||||
{"id": LyricsProviderSpotifyAPI, "name": "Spotify Lyrics API", "has_proxy_dependency": true, "description": "Spotify-sourced lyrics via Paxsenix"},
|
||||
@@ -140,7 +137,6 @@ func normalizeLyricsFetchOptions(opts LyricsFetchOptions) LyricsFetchOptions {
|
||||
return opts
|
||||
}
|
||||
|
||||
// SetLyricsFetchOptions sets provider-specific lyric fetch behavior.
|
||||
func SetLyricsFetchOptions(opts LyricsFetchOptions) {
|
||||
normalized := normalizeLyricsFetchOptions(opts)
|
||||
|
||||
@@ -156,7 +152,6 @@ func SetLyricsFetchOptions(opts LyricsFetchOptions) {
|
||||
)
|
||||
}
|
||||
|
||||
// GetLyricsFetchOptions returns current provider-specific lyric fetch behavior.
|
||||
func GetLyricsFetchOptions() LyricsFetchOptions {
|
||||
lyricsFetchOptionsMu.RLock()
|
||||
defer lyricsFetchOptionsMu.RUnlock()
|
||||
@@ -667,7 +662,6 @@ func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName st
|
||||
|
||||
GoLog("[Lyrics] Searching for: %s - %s (providers: %v)\n", artistName, trackName, providerOrder)
|
||||
|
||||
// Cascade through all configured built-in providers
|
||||
for _, providerName := range providerOrder {
|
||||
GoLog("[Lyrics] Trying provider: %s\n", providerName)
|
||||
|
||||
|
||||
@@ -542,7 +542,6 @@ func searchYouTubeMusicViaExtension(artistName, trackName string) string {
|
||||
extManager := GetExtensionManager()
|
||||
searchProviders := extManager.GetSearchProviders()
|
||||
|
||||
// Find the ytmusic-spotiflac extension
|
||||
var ytProvider *ExtensionProviderWrapper
|
||||
for _, p := range searchProviders {
|
||||
if p.extension.ID == "ytmusic-spotiflac" {
|
||||
@@ -569,7 +568,6 @@ func searchYouTubeMusicViaExtension(artistName, trackName string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find the first track result (item_type == "track" with a valid video ID)
|
||||
for _, track := range results {
|
||||
if track.ItemType != "" && track.ItemType != "track" {
|
||||
continue
|
||||
@@ -610,7 +608,6 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try Spotify ID via SongLink
|
||||
if youtubeURL == "" && req.SpotifyID != "" && !isYouTubeVideoID(req.SpotifyID) {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for Spotify ID: %s\n", req.SpotifyID)
|
||||
songlink := NewSongLinkClient()
|
||||
@@ -622,7 +619,6 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try Deezer ID via SongLink
|
||||
if youtubeURL == "" && req.DeezerID != "" {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for Deezer ID: %s\n", req.DeezerID)
|
||||
songlink := NewSongLinkClient()
|
||||
@@ -634,7 +630,6 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try ISRC via SongLink
|
||||
if youtubeURL == "" && req.ISRC != "" {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for ISRC: %s\n", req.ISRC)
|
||||
songlink := NewSongLinkClient()
|
||||
|
||||
@@ -10,7 +10,7 @@ class AppInfo {
|
||||
/// Shows "Internal" in debug builds, actual version in release.
|
||||
static String get displayVersion => kDebugMode ? 'Internal' : version;
|
||||
|
||||
static const String appName = 'SpotiFLAC';
|
||||
static const String appName = 'SpotiFLAC Mobile';
|
||||
static const String copyright = '© 2026 SpotiFLAC';
|
||||
|
||||
static const String mobileAuthor = 'zarzet';
|
||||
|
||||
@@ -192,11 +192,9 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
if (settings.localLibraryPath.isEmpty) return;
|
||||
if (settings.localLibraryAutoScan == 'off') return;
|
||||
|
||||
// Don't start a scan if one is already running.
|
||||
final libraryState = ref.read(localLibraryProvider);
|
||||
if (libraryState.isScanning) return;
|
||||
|
||||
// Determine cooldown based on auto-scan mode.
|
||||
final now = DateTime.now();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final lastScanned = readLocalLibraryLastScannedAt(prefs);
|
||||
@@ -220,7 +218,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization>
|
||||
}
|
||||
}
|
||||
|
||||
// All checks passed -- start an incremental scan.
|
||||
final iosBookmark = settings.localLibraryBookmark;
|
||||
ref
|
||||
.read(localLibraryProvider.notifier)
|
||||
|
||||
@@ -874,8 +874,9 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
await _db.upsert(updated.toJson());
|
||||
}
|
||||
|
||||
/// Remove history entries where the file no longer exists on disk
|
||||
/// Returns the number of orphaned entries removed
|
||||
/// Remove history entries where the file no longer exists on disk.
|
||||
/// Returns the number of orphaned entries removed.
|
||||
|
||||
/// Audio file extensions that the app commonly produces or converts between.
|
||||
static const _audioExtensions = [
|
||||
'.flac',
|
||||
@@ -891,7 +892,6 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
|
||||
/// different audio extension exists (e.g. the user converted .flac → .opus).
|
||||
/// Returns the path of the first match found, or `null` if none exist.
|
||||
Future<String?> _findConvertedSibling(String originalPath) async {
|
||||
// Strip the current extension to get the base path.
|
||||
final dotIndex = originalPath.lastIndexOf('.');
|
||||
if (dotIndex < 0) return null;
|
||||
final basePath = originalPath.substring(0, dotIndex);
|
||||
|
||||
@@ -3124,16 +3124,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
_triggerSearchWithFilter(null);
|
||||
},
|
||||
showCheckmark: false,
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
labelStyle: TextStyle(
|
||||
color: selectedFilter == null
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: selectedFilter == null
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
...filters.map((filter) {
|
||||
@@ -3148,24 +3138,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
_triggerSearchWithFilter(filter.id);
|
||||
},
|
||||
showCheckmark: false,
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
backgroundColor: colorScheme.surfaceContainerHighest,
|
||||
labelStyle: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
avatar: filter.icon != null
|
||||
? Icon(
|
||||
_getFilterIcon(filter.icon!),
|
||||
size: 18,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
)
|
||||
? Icon(_getFilterIcon(filter.icon!), size: 18)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
@@ -3220,15 +3194,11 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
@@ -3276,6 +3246,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
onSubmitted: (_) => _onSearchSubmitted(),
|
||||
onTapOutside: (_) {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+44
-14
@@ -31,9 +31,11 @@ class MainShell extends ConsumerStatefulWidget {
|
||||
ConsumerState<MainShell> createState() => _MainShellState();
|
||||
}
|
||||
|
||||
class _MainShellState extends ConsumerState<MainShell> {
|
||||
class _MainShellState extends ConsumerState<MainShell>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
late final PageController _pageController;
|
||||
late final AnimationController _tabJumpTransitionController;
|
||||
bool _hasCheckedUpdate = false;
|
||||
StreamSubscription<String>? _shareSubscription;
|
||||
DateTime? _lastBackPress;
|
||||
@@ -48,6 +50,11 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pageController = PageController(initialPage: _currentIndex);
|
||||
_tabJumpTransitionController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 180),
|
||||
value: 1,
|
||||
);
|
||||
ShellNavigationService.syncState(
|
||||
currentTabIndex: _currentIndex,
|
||||
showStoreTab: false,
|
||||
@@ -229,6 +236,7 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
void dispose() {
|
||||
_shareSubscription?.cancel();
|
||||
_pageController.dispose();
|
||||
_tabJumpTransitionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -251,6 +259,8 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
}
|
||||
|
||||
if (_currentIndex != index) {
|
||||
final previousIndex = _currentIndex;
|
||||
final isNonAdjacentJump = (previousIndex - index).abs() > 1;
|
||||
final shouldResetHome = index == 0;
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _currentIndex = index);
|
||||
@@ -265,11 +275,19 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
if (shouldResetHome) {
|
||||
_resetHomeToMain();
|
||||
}
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
// Jump directly when skipping intermediate tabs to avoid
|
||||
// sliding through them. For those jumps, keep a short fade-in
|
||||
// so the transition still feels intentional.
|
||||
if (isNonAdjacentJump) {
|
||||
_pageController.jumpToPage(index);
|
||||
_tabJumpTransitionController.forward(from: 0);
|
||||
} else {
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,15 +522,27 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: tabs.length,
|
||||
onPageChanged: _onPageChanged,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _KeepAliveTabPage(
|
||||
key: ValueKey('page-$index'),
|
||||
child: tabs[index],
|
||||
body: AnimatedBuilder(
|
||||
animation: _tabJumpTransitionController,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: tabs.length,
|
||||
onPageChanged: _onPageChanged,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _KeepAliveTabPage(
|
||||
key: ValueKey('page-$index'),
|
||||
child: tabs[index],
|
||||
),
|
||||
),
|
||||
builder: (context, child) {
|
||||
final t = Curves.easeOutCubic.transform(
|
||||
_tabJumpTransitionController.value,
|
||||
);
|
||||
return Opacity(
|
||||
opacity: t,
|
||||
child: Transform.scale(scale: 0.985 + (0.015 * t), child: child),
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentIndex.clamp(0, maxIndex),
|
||||
|
||||
+297
-280
@@ -2898,6 +2898,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Clear',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
@@ -2912,26 +2913,24 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2.5,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 12,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
@@ -3355,238 +3354,91 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a collection item at [index] for the unified "All" tab grid view.
|
||||
/// Index 0 = Wishlist, 1 = Loved, 2+ = individual playlists.
|
||||
/// Returns the visible collection entries, hiding Wishlist/Loved when empty.
|
||||
List<_CollectionEntry> _getVisibleCollectionEntries(
|
||||
LibraryCollectionsState collectionState,
|
||||
) {
|
||||
final entries = <_CollectionEntry>[];
|
||||
if (collectionState.wishlistCount > 0) {
|
||||
entries.add(_CollectionEntry.wishlist);
|
||||
}
|
||||
if (collectionState.lovedCount > 0) {
|
||||
entries.add(_CollectionEntry.loved);
|
||||
}
|
||||
for (var i = 0; i < collectionState.playlists.length; i++) {
|
||||
entries.add(_CollectionEntry.playlist(i));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// Build a collection item for the unified "All" tab grid view.
|
||||
Widget _buildAllTabGridCollectionItem({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
required int index,
|
||||
required _CollectionEntry entry,
|
||||
required LibraryCollectionsState collectionState,
|
||||
List<UnifiedLibraryItem> filteredUnifiedItems = const [],
|
||||
}) {
|
||||
if (index == 0) {
|
||||
return _buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.add_circle_outline,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF1DB954),
|
||||
title: context.l10n.collectionWishlist,
|
||||
count: collectionState.wishlistCount,
|
||||
onTap: _openWishlistFolder,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
return _buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.favorite,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF8C67AC),
|
||||
title: context.l10n.collectionLoved,
|
||||
count: collectionState.lovedCount,
|
||||
onTap: _openLovedFolder,
|
||||
);
|
||||
} else {
|
||||
final playlist = collectionState.playlists[index - 2];
|
||||
final isSelected = _selectedPlaylistIds.contains(playlist.id);
|
||||
return DragTarget<UnifiedLibraryItem>(
|
||||
onWillAcceptWithDetails: (_) => !_isPlaylistSelectionMode,
|
||||
onAcceptWithDetails: (details) {
|
||||
_onTrackDroppedOnPlaylist(
|
||||
context,
|
||||
details.data,
|
||||
playlist.id,
|
||||
playlist.name,
|
||||
allItems: filteredUnifiedItems,
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: isHovering
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.primary, width: 2),
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
coverWidget: _buildPlaylistCover(
|
||||
context,
|
||||
playlist,
|
||||
colorScheme,
|
||||
),
|
||||
title: playlist.name,
|
||||
count: playlist.tracks.length,
|
||||
onTap: _isPlaylistSelectionMode
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _openPlaylistById(playlist.id),
|
||||
onLongPress: _isPlaylistSelectionMode
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _enterPlaylistSelectionMode(playlist.id),
|
||||
),
|
||||
if (_isPlaylistSelectionMode)
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary.withValues(alpha: 0.3)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isPlaylistSelectionMode)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.surface.withValues(alpha: 0.85),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: colorScheme.onPrimary,
|
||||
)
|
||||
: const SizedBox(width: 16, height: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a collection item at [index] for the unified "All" tab list view.
|
||||
/// Index 0 = Wishlist, 1 = Loved, 2+ = individual playlists.
|
||||
Widget _buildAllTabListCollectionItem({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
required int index,
|
||||
required LibraryCollectionsState collectionState,
|
||||
List<UnifiedLibraryItem> filteredUnifiedItems = const [],
|
||||
}) {
|
||||
if (index == 0) {
|
||||
return _buildCollectionListItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.add_circle_outline,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF1DB954),
|
||||
title: context.l10n.collectionWishlist,
|
||||
subtitle:
|
||||
'${context.l10n.collectionFoldersTitle} • ${collectionState.wishlistCount} ${collectionState.wishlistCount == 1 ? 'track' : 'tracks'}',
|
||||
onTap: _openWishlistFolder,
|
||||
);
|
||||
} else if (index == 1) {
|
||||
return _buildCollectionListItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.favorite,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF8C67AC),
|
||||
title: context.l10n.collectionLoved,
|
||||
subtitle:
|
||||
'${context.l10n.collectionFoldersTitle} • ${collectionState.lovedCount} ${collectionState.lovedCount == 1 ? 'track' : 'tracks'}',
|
||||
onTap: _openLovedFolder,
|
||||
);
|
||||
} else {
|
||||
final playlist = collectionState.playlists[index - 2];
|
||||
final isSelected = _selectedPlaylistIds.contains(playlist.id);
|
||||
return DragTarget<UnifiedLibraryItem>(
|
||||
onWillAcceptWithDetails: (_) => !_isPlaylistSelectionMode,
|
||||
onAcceptWithDetails: (details) {
|
||||
_onTrackDroppedOnPlaylist(
|
||||
context,
|
||||
details.data,
|
||||
playlist.id,
|
||||
playlist.name,
|
||||
allItems: filteredUnifiedItems,
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: isHovering
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.primary, width: 2),
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (_isPlaylistSelectionMode)
|
||||
GestureDetector(
|
||||
onTap: () => _togglePlaylistSelection(playlist.id),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: colorScheme.onPrimary,
|
||||
)
|
||||
: const SizedBox(width: 18, height: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildCollectionListItem(
|
||||
switch (entry.type) {
|
||||
case _CollectionEntryType.wishlist:
|
||||
return _buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.add_circle_outline,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF1DB954),
|
||||
title: context.l10n.collectionWishlist,
|
||||
count: collectionState.wishlistCount,
|
||||
onTap: _openWishlistFolder,
|
||||
);
|
||||
case _CollectionEntryType.loved:
|
||||
return _buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.favorite,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF8C67AC),
|
||||
title: context.l10n.collectionLoved,
|
||||
count: collectionState.lovedCount,
|
||||
onTap: _openLovedFolder,
|
||||
);
|
||||
case _CollectionEntryType.playlist:
|
||||
final playlist = collectionState.playlists[entry.playlistIndex];
|
||||
final isSelected = _selectedPlaylistIds.contains(playlist.id);
|
||||
return DragTarget<UnifiedLibraryItem>(
|
||||
onWillAcceptWithDetails: (_) => !_isPlaylistSelectionMode,
|
||||
onAcceptWithDetails: (details) {
|
||||
_onTrackDroppedOnPlaylist(
|
||||
context,
|
||||
details.data,
|
||||
playlist.id,
|
||||
playlist.name,
|
||||
allItems: filteredUnifiedItems,
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: isHovering
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.primary, width: 2),
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
)
|
||||
: null,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildCollectionGridItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
coverWidget: _buildPlaylistCover(
|
||||
context,
|
||||
playlist,
|
||||
colorScheme,
|
||||
56,
|
||||
),
|
||||
title: playlist.name,
|
||||
subtitle:
|
||||
'${playlist.tracks.length} ${playlist.tracks.length == 1 ? 'track' : 'tracks'}',
|
||||
count: playlist.tracks.length,
|
||||
onTap: _isPlaylistSelectionMode
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _openPlaylistById(playlist.id),
|
||||
@@ -3594,12 +3446,176 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _enterPlaylistSelectionMode(playlist.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (_isPlaylistSelectionMode)
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: IgnorePointer(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary.withValues(alpha: 0.3)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isPlaylistSelectionMode)
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.surface.withValues(alpha: 0.85),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: colorScheme.onPrimary,
|
||||
)
|
||||
: const SizedBox(width: 16, height: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a collection item for the unified "All" tab list view.
|
||||
Widget _buildAllTabListCollectionItem({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
required _CollectionEntry entry,
|
||||
required LibraryCollectionsState collectionState,
|
||||
List<UnifiedLibraryItem> filteredUnifiedItems = const [],
|
||||
}) {
|
||||
switch (entry.type) {
|
||||
case _CollectionEntryType.wishlist:
|
||||
return _buildCollectionListItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.add_circle_outline,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF1DB954),
|
||||
title: context.l10n.collectionWishlist,
|
||||
subtitle:
|
||||
'${context.l10n.collectionFoldersTitle} • ${collectionState.wishlistCount} ${collectionState.wishlistCount == 1 ? 'track' : 'tracks'}',
|
||||
onTap: _openWishlistFolder,
|
||||
);
|
||||
case _CollectionEntryType.loved:
|
||||
return _buildCollectionListItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.favorite,
|
||||
iconColor: Colors.white,
|
||||
iconBgColor: const Color(0xFF8C67AC),
|
||||
title: context.l10n.collectionLoved,
|
||||
subtitle:
|
||||
'${context.l10n.collectionFoldersTitle} • ${collectionState.lovedCount} ${collectionState.lovedCount == 1 ? 'track' : 'tracks'}',
|
||||
onTap: _openLovedFolder,
|
||||
);
|
||||
case _CollectionEntryType.playlist:
|
||||
final playlist = collectionState.playlists[entry.playlistIndex];
|
||||
final isSelected = _selectedPlaylistIds.contains(playlist.id);
|
||||
return DragTarget<UnifiedLibraryItem>(
|
||||
onWillAcceptWithDetails: (_) => !_isPlaylistSelectionMode,
|
||||
onAcceptWithDetails: (details) {
|
||||
_onTrackDroppedOnPlaylist(
|
||||
context,
|
||||
details.data,
|
||||
playlist.id,
|
||||
playlist.name,
|
||||
allItems: filteredUnifiedItems,
|
||||
);
|
||||
},
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
decoration: isHovering
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: colorScheme.primary, width: 2),
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
if (_isPlaylistSelectionMode)
|
||||
GestureDetector(
|
||||
onTap: () => _togglePlaylistSelection(playlist.id),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: colorScheme.onPrimary,
|
||||
)
|
||||
: const SizedBox(width: 18, height: 18),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _buildCollectionListItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
coverWidget: _buildPlaylistCover(
|
||||
context,
|
||||
playlist,
|
||||
colorScheme,
|
||||
56,
|
||||
),
|
||||
title: playlist.name,
|
||||
subtitle:
|
||||
'${playlist.tracks.length} ${playlist.tracks.length == 1 ? 'track' : 'tracks'}',
|
||||
onTap: _isPlaylistSelectionMode
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _openPlaylistById(playlist.id),
|
||||
onLongPress: _isPlaylistSelectionMode
|
||||
? () => _togglePlaylistSelection(playlist.id)
|
||||
: () => _enterPlaylistSelectionMode(playlist.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3789,13 +3805,15 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final collectionCount =
|
||||
2 + collectionState.playlists.length;
|
||||
final collectionEntries = _getVisibleCollectionEntries(
|
||||
collectionState,
|
||||
);
|
||||
final collectionCount = collectionEntries.length;
|
||||
if (index < collectionCount) {
|
||||
return _buildAllTabGridCollectionItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
index: index,
|
||||
entry: collectionEntries[index],
|
||||
collectionState: collectionState,
|
||||
filteredUnifiedItems: filteredUnifiedItems,
|
||||
);
|
||||
@@ -3831,8 +3849,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
childCount:
|
||||
2 +
|
||||
collectionState.playlists.length +
|
||||
_getVisibleCollectionEntries(collectionState).length +
|
||||
filteredUnifiedItems.length,
|
||||
),
|
||||
),
|
||||
@@ -3841,12 +3858,15 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final collectionCount = 2 + collectionState.playlists.length;
|
||||
final collectionEntries = _getVisibleCollectionEntries(
|
||||
collectionState,
|
||||
);
|
||||
final collectionCount = collectionEntries.length;
|
||||
if (index < collectionCount) {
|
||||
return _buildAllTabListCollectionItem(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
index: index,
|
||||
entry: collectionEntries[index],
|
||||
collectionState: collectionState,
|
||||
filteredUnifiedItems: filteredUnifiedItems,
|
||||
);
|
||||
@@ -3882,8 +3902,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
childCount:
|
||||
2 +
|
||||
collectionState.playlists.length +
|
||||
_getVisibleCollectionEntries(collectionState).length +
|
||||
filteredUnifiedItems.length,
|
||||
),
|
||||
),
|
||||
@@ -6372,6 +6391,20 @@ class _QueueItemSliverRow extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
enum _CollectionEntryType { wishlist, loved, playlist }
|
||||
|
||||
class _CollectionEntry {
|
||||
final _CollectionEntryType type;
|
||||
final int playlistIndex;
|
||||
|
||||
const _CollectionEntry._(this.type, [this.playlistIndex = -1]);
|
||||
|
||||
static const wishlist = _CollectionEntry._(_CollectionEntryType.wishlist);
|
||||
static const loved = _CollectionEntry._(_CollectionEntryType.loved);
|
||||
static _CollectionEntry playlist(int index) =>
|
||||
_CollectionEntry._(_CollectionEntryType.playlist, index);
|
||||
}
|
||||
|
||||
class _FilterChip extends StatelessWidget {
|
||||
final String label;
|
||||
final int count;
|
||||
@@ -6389,52 +6422,36 @@ class _FilterChip extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Material(
|
||||
color: isSelected
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
return FilterChip(
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(label),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary.withValues(alpha: 0.2)
|
||||
: colorScheme.outline.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primary.withValues(alpha: 0.2)
|
||||
: colorScheme.outline.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
count.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
selected: isSelected,
|
||||
onSelected: (_) => onTap(),
|
||||
showCheckmark: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-19
@@ -58,7 +58,9 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
final downloadingId = ref.watch(
|
||||
storeProvider.select((s) => s.downloadingId),
|
||||
);
|
||||
final hasRegistryUrl = ref.watch(storeProvider.select((s) => s.hasRegistryUrl));
|
||||
final hasRegistryUrl = ref.watch(
|
||||
storeProvider.select((s) => s.hasRegistryUrl),
|
||||
);
|
||||
final registryUrl = ref.watch(storeProvider.select((s) => s.registryUrl));
|
||||
final filteredExtensions = StoreState(
|
||||
extensions: extensions,
|
||||
@@ -139,7 +141,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: value.text.isNotEmpty
|
||||
? IconButton(
|
||||
tooltip: 'Clear search',
|
||||
tooltip: 'Clear',
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
@@ -151,23 +153,37 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide.none,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor:
|
||||
Theme.of(context).brightness == Brightness.dark
|
||||
? Color.alphaBlend(
|
||||
Colors.white.withValues(alpha: 0.08),
|
||||
colorScheme.surface,
|
||||
)
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
horizontal: 20,
|
||||
vertical: 16,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
ref.read(storeProvider.notifier).setSearchQuery(value);
|
||||
ref
|
||||
.read(storeProvider.notifier)
|
||||
.setSearchQuery(value);
|
||||
},
|
||||
onTapOutside: (_) {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -231,7 +247,8 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
_CategoryChip(
|
||||
label: context.l10n.storeFilterIntegration,
|
||||
icon: Icons.link,
|
||||
isSelected: selectedCategory == StoreCategory.integration,
|
||||
isSelected:
|
||||
selectedCategory == StoreCategory.integration,
|
||||
onTap: () => ref
|
||||
.read(storeProvider.notifier)
|
||||
.setCategory(StoreCategory.integration),
|
||||
@@ -309,9 +326,9 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
context.l10n.storeAddRepoTitle,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
@@ -347,7 +364,11 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 20, color: colorScheme.onErrorContainer),
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 20,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -503,7 +524,9 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
hasFilters ? context.l10n.storeEmptyNoResults : context.l10n.storeEmptyNoExtensions,
|
||||
hasFilters
|
||||
? context.l10n.storeEmptyNoResults
|
||||
: context.l10n.storeEmptyNoExtensions,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user