diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5fc4b2f..75f1eaed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,54 @@
# Changelog
+## [1.5.5] - 2026-01-02
+
+### Added
+- **Share to App**: Share Spotify links directly from Spotify app or browser to SpotiFLAC
+ - Supports track, album, playlist, and artist URLs
+ - Auto-fetches metadata when link is shared
+ - Works with both `open.spotify.com` URLs and `spotify:` URIs
+- **Lyrics Viewer**: View lyrics for downloaded tracks in Track Metadata screen
+ - Fetches lyrics from LRCLIB on-demand
+ - Clean display without timestamps
+ - Copy lyrics to clipboard
+- **Artist URL Support**: Paste artist URL to browse their discography
+ - Shows all albums, singles, and compilations
+ - Horizontal scrollable album cards grouped by type
+ - Tap any album to view and download its tracks
+- **Folder Organization**: Organize downloads into folders by artist or album
+ - Options: None, By Artist, By Album, By Artist & Album
+ - Configurable in Settings > Download
+- **Japanese Lyrics to Romaji**: Auto-convert Hiragana/Katakana lyrics to romaji
+ - Useful for non-Japanese speakers who want to sing along
+ - Toggle in Settings > Options > Lyrics
+ - Kanji characters are preserved (requires dictionary lookup)
+- **History View Mode**: Choose between grid or list view for download history
+ - Grid view shows album art in a 3-column layout (default)
+ - List view shows detailed track info with date
+ - Configurable in Settings > Appearance > Layout
+- **Exit Confirmation**: Dialog prompt when pressing back to exit app (only at root)
+
+### Changed
+- **Downloads Tab Renamed to History**: Better reflects the tab's purpose
+ - Shows download queue at top when active
+ - Completed downloads auto-move to history section
+ - Cleaner separation between active downloads and history
+- **Smarter Back Navigation**: Back button now navigates properly
+ - Goes back through search history (album → artist → empty)
+ - Returns to Search tab from other tabs
+ - Only shows exit dialog when truly at root
+
+### Fixed
+- **Download Progress**: Fixed progress stuck at 0% when using item-based progress tracking (affected sequential downloads after multi-download feature was added)
+- **Artist View State**: Fixed UI state not clearing properly when switching between artist and album views
+- **Share Intent Timing**: Fixed shared URLs not being processed when app was cold-started from share intent
+
+### Improved
+- **Cleaner UI for Returning Users**: Helper text "Supports: Track, Album, Playlist URLs" now only shows for new users and hides after first search
+- **Cleaner Home Tab**: Removed redundant "Recent Downloads" section, renamed to "Search" tab
+- **Centered Search Bar**: Search bar now appears centered on screen when empty, moves to top when results are shown - easier to reach on large phones
+- **Back Navigation**: Android back button now works as expected - returns to previous view (album → artist → empty search)
+
## [1.5.0-hotfix6] - 2026-01-02
### Fixed
diff --git a/README.md b/README.md
index 1ecb3348..91333ad3 100644
--- a/README.md
+++ b/README.md
@@ -26,10 +26,10 @@ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account
## Screenshots
-
-
-
-
+
+
+
+
## Other project
diff --git a/assets/images/1.jpg b/assets/images/1.jpg
new file mode 100644
index 00000000..64749dcb
Binary files /dev/null and b/assets/images/1.jpg differ
diff --git a/assets/images/2.jpg b/assets/images/2.jpg
new file mode 100644
index 00000000..c509b212
Binary files /dev/null and b/assets/images/2.jpg differ
diff --git a/assets/images/3.jpg b/assets/images/3.jpg
new file mode 100644
index 00000000..09d59ab2
Binary files /dev/null and b/assets/images/3.jpg differ
diff --git a/assets/images/4.jpg b/assets/images/4.jpg
new file mode 100644
index 00000000..3afce344
Binary files /dev/null and b/assets/images/4.jpg differ
diff --git a/assets/images/photo_2026-01-02_02-35-09.jpg b/assets/images/photo_2026-01-02_02-35-09.jpg
deleted file mode 100644
index bb4001bd..00000000
Binary files a/assets/images/photo_2026-01-02_02-35-09.jpg and /dev/null differ
diff --git a/assets/images/photo_2026-01-02_02-35-34.jpg b/assets/images/photo_2026-01-02_02-35-34.jpg
deleted file mode 100644
index b2816252..00000000
Binary files a/assets/images/photo_2026-01-02_02-35-34.jpg and /dev/null differ
diff --git a/assets/images/photo_2026-01-02_02-35-37.jpg b/assets/images/photo_2026-01-02_02-35-37.jpg
deleted file mode 100644
index 852e2777..00000000
Binary files a/assets/images/photo_2026-01-02_02-35-37.jpg and /dev/null differ
diff --git a/assets/images/photo_2026-01-02_02-36-23.jpg b/assets/images/photo_2026-01-02_02-36-23.jpg
deleted file mode 100644
index 59a75d94..00000000
Binary files a/assets/images/photo_2026-01-02_02-36-23.jpg and /dev/null differ
diff --git a/go_backend/amazon.go b/go_backend/amazon.go
index 9148e2f9..3270b71b 100644
--- a/go_backend/amazon.go
+++ b/go_backend/amazon.go
@@ -364,6 +364,17 @@ func downloadFromAmazon(req DownloadRequest) (string, error) {
fmt.Println("[Amazon] No lyrics found for this track")
} else {
fmt.Printf("[Amazon] Lyrics found (%d lines), embedding...\n", len(lyrics.Lines))
+
+ // Convert Japanese lyrics to romaji if enabled
+ if req.ConvertLyricsToRomaji {
+ for i := range lyrics.Lines {
+ if ContainsKana(lyrics.Lines[i].Words) {
+ lyrics.Lines[i].Words = ToRomaji(lyrics.Lines[i].Words)
+ }
+ }
+ fmt.Println("[Amazon] Converted Japanese lyrics to romaji")
+ }
+
lrcContent := convertToLRC(lyrics)
if embedErr := EmbedLyrics(outputPath, lrcContent); embedErr != nil {
fmt.Printf("[Amazon] Warning: failed to embed lyrics: %v\n", embedErr)
diff --git a/go_backend/exports.go b/go_backend/exports.go
index 1e81b92f..a472fbb8 100644
--- a/go_backend/exports.go
+++ b/go_backend/exports.go
@@ -102,6 +102,7 @@ type DownloadRequest struct {
Quality string `json:"quality"` // LOSSLESS, HI_RES, HI_RES_LOSSLESS
EmbedLyrics bool `json:"embed_lyrics"`
EmbedMaxQualityCover bool `json:"embed_max_quality_cover"`
+ ConvertLyricsToRomaji bool `json:"convert_lyrics_to_romaji"`
TrackNumber int `json:"track_number"`
DiscNumber int `json:"disc_number"`
TotalTracks int `json:"total_tracks"`
@@ -373,6 +374,12 @@ func EmbedLyricsToFile(filePath, lyrics string) (string, error) {
return string(jsonBytes), nil
}
+// ConvertToRomaji converts Japanese kana (Hiragana/Katakana) to romaji
+// Kanji characters are preserved as-is
+func ConvertToRomaji(text string) string {
+ return ToRomaji(text)
+}
+
func errorResponse(msg string) (string, error) {
resp := DownloadResponse{
Success: false,
diff --git a/go_backend/progress.go b/go_backend/progress.go
index 0cf546ce..67975fbc 100644
--- a/go_backend/progress.go
+++ b/go_backend/progress.go
@@ -267,5 +267,7 @@ func (pw *ItemProgressWriter) Write(p []byte) (int, error) {
}
pw.current += int64(n)
SetItemBytesReceived(pw.itemID, pw.current)
+ // Also update legacy progress for backward compatibility
+ SetBytesReceived(pw.current)
return n, nil
}
diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go
index b8b4ddb7..be1e9750 100644
--- a/go_backend/qobuz.go
+++ b/go_backend/qobuz.go
@@ -426,6 +426,17 @@ func downloadFromQobuz(req DownloadRequest) (string, error) {
fmt.Println("[Qobuz] No lyrics found for this track")
} else {
fmt.Printf("[Qobuz] Lyrics found (%d lines), embedding...\n", len(lyrics.Lines))
+
+ // Convert Japanese lyrics to romaji if enabled
+ if req.ConvertLyricsToRomaji {
+ for i := range lyrics.Lines {
+ if ContainsKana(lyrics.Lines[i].Words) {
+ lyrics.Lines[i].Words = ToRomaji(lyrics.Lines[i].Words)
+ }
+ }
+ fmt.Println("[Qobuz] Converted Japanese lyrics to romaji")
+ }
+
lrcContent := convertToLRC(lyrics)
if embedErr := EmbedLyrics(outputPath, lrcContent); embedErr != nil {
fmt.Printf("[Qobuz] Warning: failed to embed lyrics: %v\n", embedErr)
diff --git a/go_backend/spotify.go b/go_backend/spotify.go
index 745a21ac..ed5b49e6 100644
--- a/go_backend/spotify.go
+++ b/go_backend/spotify.go
@@ -20,6 +20,8 @@ const (
playlistBaseURL = "https://api.spotify.com/v1/playlists/%s"
albumBaseURL = "https://api.spotify.com/v1/albums/%s"
trackBaseURL = "https://api.spotify.com/v1/tracks/%s"
+ artistBaseURL = "https://api.spotify.com/v1/artists/%s"
+ artistAlbumsURL = "https://api.spotify.com/v1/artists/%s/albums"
searchBaseURL = "https://api.spotify.com/v1/search"
)
@@ -131,6 +133,32 @@ type PlaylistResponsePayload struct {
TrackList []AlbumTrackMetadata `json:"track_list"`
}
+// ArtistInfoMetadata holds artist information
+type ArtistInfoMetadata struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Images string `json:"images"`
+ Followers int `json:"followers"`
+ Popularity int `json:"popularity"`
+}
+
+// ArtistAlbumMetadata holds album info for artist discography
+type ArtistAlbumMetadata struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ReleaseDate string `json:"release_date"`
+ TotalTracks int `json:"total_tracks"`
+ Images string `json:"images"`
+ AlbumType string `json:"album_type"` // album, single, compilation
+ Artists string `json:"artists"`
+}
+
+// ArtistResponsePayload is the response for artist requests
+type ArtistResponsePayload struct {
+ ArtistInfo ArtistInfoMetadata `json:"artist_info"`
+ Albums []ArtistAlbumMetadata `json:"albums"`
+}
+
// TrackResponse is the response for single track requests
type TrackResponse struct {
Track TrackMetadata `json:"track"`
@@ -212,6 +240,8 @@ func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL
return c.fetchAlbum(ctx, parsed.ID, token)
case "playlist":
return c.fetchPlaylist(ctx, parsed.ID, token)
+ case "artist":
+ return c.fetchArtist(ctx, parsed.ID, token)
default:
return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type)
}
@@ -405,6 +435,88 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID, t
}, nil
}
+func (c *SpotifyMetadataClient) fetchArtist(ctx context.Context, artistID, token string) (*ArtistResponsePayload, error) {
+ // Fetch artist info
+ var artistData struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Images []image `json:"images"`
+ Followers struct {
+ Total int `json:"total"`
+ } `json:"followers"`
+ Popularity int `json:"popularity"`
+ }
+
+ if err := c.getJSON(ctx, fmt.Sprintf(artistBaseURL, artistID), token, &artistData); err != nil {
+ return nil, err
+ }
+
+ artistInfo := ArtistInfoMetadata{
+ ID: artistData.ID,
+ Name: artistData.Name,
+ Images: firstImageURL(artistData.Images),
+ Followers: artistData.Followers.Total,
+ Popularity: artistData.Popularity,
+ }
+
+ // Fetch artist albums (all types: album, single, compilation)
+ albums := make([]ArtistAlbumMetadata, 0)
+ offset := 0
+ limit := 50
+
+ for {
+ albumsURL := fmt.Sprintf("%s?include_groups=album,single,compilation&limit=%d&offset=%d",
+ fmt.Sprintf(artistAlbumsURL, artistID), limit, offset)
+
+ var albumsData struct {
+ Items []struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ ReleaseDate string `json:"release_date"`
+ TotalTracks int `json:"total_tracks"`
+ Images []image `json:"images"`
+ AlbumType string `json:"album_type"`
+ Artists []artist `json:"artists"`
+ ExternalURL externalURL `json:"external_urls"`
+ } `json:"items"`
+ Next string `json:"next"`
+ Total int `json:"total"`
+ }
+
+ if err := c.getJSON(ctx, albumsURL, token, &albumsData); err != nil {
+ return nil, err
+ }
+
+ for _, album := range albumsData.Items {
+ albums = append(albums, ArtistAlbumMetadata{
+ ID: album.ID,
+ Name: album.Name,
+ ReleaseDate: album.ReleaseDate,
+ TotalTracks: album.TotalTracks,
+ Images: firstImageURL(album.Images),
+ AlbumType: album.AlbumType,
+ Artists: joinArtists(album.Artists),
+ })
+ }
+
+ // Check if there are more albums
+ if albumsData.Next == "" || len(albumsData.Items) < limit {
+ break
+ }
+ offset += limit
+
+ // Safety limit to prevent infinite loops
+ if offset > 500 {
+ break
+ }
+ }
+
+ return &ArtistResponsePayload{
+ ArtistInfo: artistInfo,
+ Albums: albums,
+ }, nil
+}
+
func (c *SpotifyMetadataClient) fetchTrackISRC(ctx context.Context, trackID, token string) string {
var data struct {
ExternalID externalID `json:"external_ids"`
diff --git a/go_backend/tidal.go b/go_backend/tidal.go
index c6b216f5..c28cf9c2 100644
--- a/go_backend/tidal.go
+++ b/go_backend/tidal.go
@@ -961,6 +961,17 @@ func downloadFromTidal(req DownloadRequest) (string, error) {
fmt.Println("[Tidal] No lyrics found for this track")
} else {
fmt.Printf("[Tidal] Lyrics found (%d lines), embedding...\n", len(lyrics.Lines))
+
+ // Convert Japanese lyrics to romaji if enabled
+ if req.ConvertLyricsToRomaji {
+ for i := range lyrics.Lines {
+ if ContainsKana(lyrics.Lines[i].Words) {
+ lyrics.Lines[i].Words = ToRomaji(lyrics.Lines[i].Words)
+ }
+ }
+ fmt.Println("[Tidal] Converted Japanese lyrics to romaji")
+ }
+
lrcContent := convertToLRC(lyrics)
if embedErr := EmbedLyrics(actualOutputPath, lrcContent); embedErr != nil {
fmt.Printf("[Tidal] Warning: failed to embed lyrics: %v\n", embedErr)
diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart
index 973f36b4..10b74e8d 100644
--- a/lib/constants/app_info.dart
+++ b/lib/constants/app_info.dart
@@ -1,8 +1,8 @@
/// App version and info constants
/// Update version here only - all other files will reference this
class AppInfo {
- static const String version = '1.5.0-hotfix6';
- static const String buildNumber = '20';
+ static const String version = '1.5.5';
+ static const String buildNumber = '22';
static const String fullVersion = '$version+$buildNumber';
static const String appName = 'SpotiFLAC';
diff --git a/lib/main.dart b/lib/main.dart
index 4e8fe0d6..dbdb3dfd 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/app.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/services/notification_service.dart';
+import 'package:spotiflac_android/services/share_intent_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -10,6 +11,9 @@ void main() async {
// Initialize notification service
await NotificationService().initialize();
+ // Initialize share intent service
+ await ShareIntentService().initialize();
+
runApp(
ProviderScope(
child: const _EagerInitialization(
diff --git a/lib/models/settings.dart b/lib/models/settings.dart
index af120301..6c8d8c66 100644
--- a/lib/models/settings.dart
+++ b/lib/models/settings.dart
@@ -14,6 +14,10 @@ class AppSettings {
final bool isFirstLaunch;
final int concurrentDownloads; // 1 = sequential (default), max 3
final bool checkForUpdates; // Check for updates on app start
+ final bool hasSearchedBefore; // Hide helper text after first search
+ final String folderOrganization; // none, artist, album, artist_album
+ final bool convertLyricsToRomaji; // Convert Japanese lyrics to romaji
+ final String historyViewMode; // list, grid
const AppSettings({
this.defaultService = 'tidal',
@@ -26,6 +30,10 @@ class AppSettings {
this.isFirstLaunch = true,
this.concurrentDownloads = 1, // Default: sequential (off)
this.checkForUpdates = true, // Default: enabled
+ this.hasSearchedBefore = false, // Default: show helper text
+ this.folderOrganization = 'none', // Default: no folder organization
+ this.convertLyricsToRomaji = false, // Default: keep original Japanese
+ this.historyViewMode = 'grid', // Default: grid view
});
AppSettings copyWith({
@@ -39,6 +47,10 @@ class AppSettings {
bool? isFirstLaunch,
int? concurrentDownloads,
bool? checkForUpdates,
+ bool? hasSearchedBefore,
+ String? folderOrganization,
+ bool? convertLyricsToRomaji,
+ String? historyViewMode,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -51,6 +63,10 @@ class AppSettings {
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads,
checkForUpdates: checkForUpdates ?? this.checkForUpdates,
+ hasSearchedBefore: hasSearchedBefore ?? this.hasSearchedBefore,
+ folderOrganization: folderOrganization ?? this.folderOrganization,
+ convertLyricsToRomaji: convertLyricsToRomaji ?? this.convertLyricsToRomaji,
+ historyViewMode: historyViewMode ?? this.historyViewMode,
);
}
diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart
index 564baae3..7821fbf0 100644
--- a/lib/models/settings.g.dart
+++ b/lib/models/settings.g.dart
@@ -17,6 +17,10 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings(
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
concurrentDownloads: (json['concurrentDownloads'] as num?)?.toInt() ?? 1,
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
+ hasSearchedBefore: json['hasSearchedBefore'] as bool? ?? false,
+ folderOrganization: json['folderOrganization'] as String? ?? 'none',
+ convertLyricsToRomaji: json['convertLyricsToRomaji'] as bool? ?? false,
+ historyViewMode: json['historyViewMode'] as String? ?? 'list',
);
Map _$AppSettingsToJson(AppSettings instance) =>
@@ -31,4 +35,8 @@ Map _$AppSettingsToJson(AppSettings instance) =>
'isFirstLaunch': instance.isFirstLaunch,
'concurrentDownloads': instance.concurrentDownloads,
'checkForUpdates': instance.checkForUpdates,
+ 'hasSearchedBefore': instance.hasSearchedBefore,
+ 'folderOrganization': instance.folderOrganization,
+ 'convertLyricsToRomaji': instance.convertLyricsToRomaji,
+ 'historyViewMode': instance.historyViewMode,
};
diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart
index 4f8b3ac2..443f5dec 100644
--- a/lib/providers/download_queue_provider.dart
+++ b/lib/providers/download_queue_provider.dart
@@ -386,6 +386,52 @@ class DownloadQueueNotifier extends Notifier {
state = state.copyWith(outputDir: dir);
}
+ /// Build output directory based on folder organization setting
+ Future _buildOutputDir(Track track, String folderOrganization) async {
+ String baseDir = state.outputDir;
+
+ if (folderOrganization == 'none') {
+ return baseDir;
+ }
+
+ // Sanitize folder names (remove invalid characters)
+ String sanitize(String name) {
+ return name
+ .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_')
+ .replaceAll(RegExp(r'\.+$'), '') // Remove trailing dots
+ .trim();
+ }
+
+ String subPath = '';
+ switch (folderOrganization) {
+ case 'artist':
+ final artistName = sanitize(track.albumArtist ?? track.artistName);
+ subPath = artistName;
+ break;
+ case 'album':
+ final albumName = sanitize(track.albumName);
+ subPath = albumName;
+ break;
+ case 'artist_album':
+ final artistName = sanitize(track.albumArtist ?? track.artistName);
+ final albumName = sanitize(track.albumName);
+ subPath = '$artistName${Platform.pathSeparator}$albumName';
+ break;
+ }
+
+ if (subPath.isNotEmpty) {
+ final fullPath = '$baseDir${Platform.pathSeparator}$subPath';
+ final dir = Directory(fullPath);
+ if (!await dir.exists()) {
+ await dir.create(recursive: true);
+ print('[DownloadQueue] Created folder: $fullPath');
+ }
+ return fullPath;
+ }
+
+ return baseDir;
+ }
+
void updateSettings(AppSettings settings) {
state = state.copyWith(
outputDir: settings.downloadDirectory.isNotEmpty ? settings.downloadDirectory : state.outputDir,
@@ -760,11 +806,16 @@ class DownloadQueueNotifier extends Notifier {
updateItemStatus(item.id, DownloadStatus.downloading);
try {
+ // Get folder organization setting and build output directory
+ final settings = ref.read(settingsProvider);
+ final outputDir = await _buildOutputDir(item.track, settings.folderOrganization);
+
Map result;
if (state.autoFallback) {
print('[DownloadQueue] Using auto-fallback mode');
print('[DownloadQueue] Quality: ${state.audioQuality}');
+ print('[DownloadQueue] Output dir: $outputDir');
result = await PlatformBridge.downloadWithFallback(
isrc: item.track.isrc ?? '',
spotifyId: item.track.id,
@@ -773,7 +824,7 @@ class DownloadQueueNotifier extends Notifier {
albumName: item.track.albumName,
albumArtist: item.track.albumArtist,
coverUrl: item.track.coverUrl,
- outputDir: state.outputDir,
+ outputDir: outputDir,
filenameFormat: state.filenameFormat,
quality: state.audioQuality,
trackNumber: item.track.trackNumber ?? 1,
@@ -781,6 +832,7 @@ class DownloadQueueNotifier extends Notifier {
releaseDate: item.track.releaseDate,
preferredService: item.service,
itemId: item.id, // Pass item ID for progress tracking
+ convertLyricsToRomaji: settings.convertLyricsToRomaji,
);
} else {
result = await PlatformBridge.downloadTrack(
@@ -792,13 +844,14 @@ class DownloadQueueNotifier extends Notifier {
albumName: item.track.albumName,
albumArtist: item.track.albumArtist,
coverUrl: item.track.coverUrl,
- outputDir: state.outputDir,
+ outputDir: outputDir,
filenameFormat: state.filenameFormat,
quality: state.audioQuality,
trackNumber: item.track.trackNumber ?? 1,
discNumber: item.track.discNumber ?? 1,
releaseDate: item.track.releaseDate,
itemId: item.id, // Pass item ID for progress tracking
+ convertLyricsToRomaji: settings.convertLyricsToRomaji,
);
}
@@ -873,6 +926,9 @@ class DownloadQueueNotifier extends Notifier {
quality: state.audioQuality,
),
);
+
+ // Auto-remove completed item from queue (it's now in history)
+ removeItem(item.id);
}
} else {
final errorMsg = result['error'] as String? ?? 'Download failed';
diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart
index 403b3d8e..31e3adc7 100644
--- a/lib/providers/settings_provider.dart
+++ b/lib/providers/settings_provider.dart
@@ -76,6 +76,28 @@ class SettingsNotifier extends Notifier {
state = state.copyWith(checkForUpdates: enabled);
_saveSettings();
}
+
+ void setHasSearchedBefore() {
+ if (!state.hasSearchedBefore) {
+ state = state.copyWith(hasSearchedBefore: true);
+ _saveSettings();
+ }
+ }
+
+ void setFolderOrganization(String organization) {
+ state = state.copyWith(folderOrganization: organization);
+ _saveSettings();
+ }
+
+ void setConvertLyricsToRomaji(bool enabled) {
+ state = state.copyWith(convertLyricsToRomaji: enabled);
+ _saveSettings();
+ }
+
+ void setHistoryViewMode(String mode) {
+ state = state.copyWith(historyViewMode: mode);
+ _saveSettings();
+ }
}
final settingsProvider = NotifierProvider(
diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart
index 2a75c7cd..8ff1eba0 100644
--- a/lib/providers/track_provider.dart
+++ b/lib/providers/track_provider.dart
@@ -8,7 +8,10 @@ class TrackState {
final String? error;
final String? albumName;
final String? playlistName;
+ final String? artistName;
final String? coverUrl;
+ final List? artistAlbums; // For artist page
+ final TrackState? previousState; // For back navigation
const TrackState({
this.tracks = const [],
@@ -16,16 +19,27 @@ class TrackState {
this.error,
this.albumName,
this.playlistName,
+ this.artistName,
this.coverUrl,
+ this.artistAlbums,
+ this.previousState,
});
+ bool get canGoBack => previousState != null;
+
+ bool get hasContent => tracks.isNotEmpty || artistAlbums != null;
+
TrackState copyWith({
List