Padrão: MAJOR.MINOR.PATCH
- MAJOR — Mudanças que quebram compatibilidade / refatoração arquitetural
- MINOR — Novas funcionalidades sem quebrar o que já existe
- PATCH — Correções de bugs, ajustes de UI, melhorias de performance
LyricsFetchService.fetch() deixou de ser cascata sequencial. Antes, encontrar a letra exigia até 5 idas à rede em série (/get exato → /get limpo → /search estratégia 1 → 2 → 3), cada uma esperando a anterior. Como o /get do LRCLIB exige duração exata (que o ID3 local quase nunca tem certa), a maioria das músicas caía até ao fim da fila — daí o “demora um tempinho”./get exato) e o fuzzy (/search estruturado) disparam ao mesmo tempo (_fetchExactSafe + _runSearches). A maioria das músicas resolve agora num único round-trip, mesmo quando o /get dá 404./get sem feat./- Remaster/(Live)… + termos livres) também em paralelo.search() (seletor manual) paraleliza as 3 estratégias via Future.wait, sem o antigo gating sequencial if hits.length < N._matches) e a distinção rede vs. “sem letra” continuam intactos — o 404 do /get é tratado como inconclusivo; só o /search decide que não existe letra, então uma falha de rede nunca marca a música como “sem letra”.flutter analyze no serviço: 0 issues._searchOnline capturava a música no início, mas depois do await gravava no song atual do provider (já trocado para a faixa nova), não na de origem.LyricsNotifier — novos applyOnlineResult(songId, lines)/markOnlineMissFor(songId) persistem sempre na chave correta e só atualizam a UI se a faixa ainda for aquela (state.songId == songId). As telas capturam o targetId antes do await e só mostram feedback se a música não mudou.List<LyricLine> direto (sem roundtrip LRC text→parse).MediaBarStyle expandido de 6 → 17 estilos. Além dos cinco sliders simples (Minimal, Brilho, Gradiente, Espessa, Clássico) e da Onda sonora já existente, agora há 11 visualizadores interativos inspirados em designs de áudio profissionais, cada um podendo ser escolhido em Configurações → Barra de progresso:
MediaSeekBar (lib/presentation/widgets/media_seek_bar.dart) centraliza render + gestos + animação; o _ProgressBar do Now Playing e o _ProgressSection do Modo Carro passaram a delegá-la. O Modo Carro agora respeita o estilo escolhido (antes ignorava e usava sempre um slider simples), derivando os tons do gradiente a partir do acento sobre fundo escuro._WaveformSeekBar/_WaveformPainter/_GradientSliderTrackShape duplicados de now_playing_page.dart; a lógica de SliderTheme dos cinco estilos simples também migrou para a widget partilhada (zero divergência entre as duas telas).mediaBar* em PT e EN.LyricsFetchService.fetch()/search() engoliam timeout/erro de rede e retornavam null — o mesmo retorno de “esta música não tem letra”. O chamador então gravava permanentemente markOnlineMiss(), obrigando o utilizador a buscar 2-3× até cair numa janela com rede boa.LyricsNetworkException, lançada após esgotar 3 tentativas com backoff (350ms→1400ms) em timeout/socket/429/5xx. fetch/search só retornam null em miss definitivo (servidor respondeu sem letra). Timeout por pedido 10s → 12s._searchOnline, diálogo de busca manual _run, Modo Carro): em exceção de rede não marcam “sem letra” — mostram “sem conexão” e voltam a buscar na próxima abertura/reprodução.cos/sin no butterfly a cada um de ~5000 frames/música (AudioAnalysisPlugin.kt) — o gargalo que fazia a análise demorar dezenas de segundos. Agora usa twiddle factors + índices de bit-reversal pré-computados por tamanho (FftPlan/planFor) e janela Hann pré-computada no BPM. ~3-5× mais rápida, mesmo resultado.MediaCodec em paralelo com o playback). Agora só corre quando o badge de análise do Now Playing fica visível. Removido o gatilho automático em PlayerNotifier._loadAndPlay.NativeAudioAnalysisService.analyze — o spinner do badge nunca gira eternamente (mídia malformada/codec lento → estado de retry).play() não produzia som — só forçar o fecho (ou limpar dados) resolvia.idle), com play() virando no-op silencioso. A pressão sobre o pool de MediaCodec pela análise DSP automática a cada faixa (ver acima) agravava o quadro.PlayerNotifier.recoverIfNeeded(), chamado no resumed do AppShell: se houver faixa carregada mas o handler estiver idle, re-prepara a faixa na última posição (pausada) para que o play() volte a funcionar sem reiniciar o processo. Combinado com a análise agora sob demanda, remove o principal estressor de codecs.ProcessingState.completed, o pipeline de áudio do just_audio é encerrado no Android. seek(zero)+play deixava o player em estado mudo (posição avançava, sem som). Corrigido: _onPlaybackCompleted chama _loadAndPlay() em RepeatMode.one, que reinicializa o source e chama resetVolume().resetVolume() adicionado antes de seek nos dois paths de navegação rápida (prev/next já em progresso), garantindo que o volume não fica em 0 quando o crossfade foi interrompido a meio.#000000 para #1A1A2E._SongPoster reformulado: arte de capa edge-to-edge no topo (1080 px), degradê inferior escuro sobre ela, conteúdo posicionado absolutamente (Positioned) com spaceBetween entre (TopBar + TitleBlock) e (Waveform + Branding). Elimina o espaço em branco desperdiciado (~400 px) que existia antes com Spacer()._LyricsPoster redesenhado para replicar o visual do ecrã de letras do app: fundo _LyricsBackground (arte a 22% opacidade + sobreposição preta uniforme + dois blobs de cor nos cantos). Elimina o blob/mancha no centro causado pelo RadialGradient antigo._LyricsHeader novo: miniatura 112 px + barra de acento + título/subtítulo — substitui o cabeçalho vazio anterior._LyricsLinesView: linhas de letra centradas com espaçamento fixo de 28 px (MainAxisAlignment.center) em vez de spaceEvenly que deixava espaço excessivo em baixo._PosterBackground e _ArtCard.constanza_history → constanza_play_history): o histórico de reprodução nunca era exportado nem importado. Corrigido em _exportedKeys do BackupService.importFromString(), o app não atualizava o estado em memória — as preferências importadas só apareciam após reiniciar o app. Corrigido: settings_page.dart agora chama themeProvider.reloadFromStorage(), audioSettingsProvider.reloadFromStorage(), playlistProvider.loadFromStorage() e libraryProvider.reloadFromBackup() imediatamente após a importação._playlistsRestored bloqueava restauração de playlists após rescan: a flag só era verdadeira uma vez; quando o backup disparava um novo rescan da biblioteca, as músicas das playlists nunca eram resolvidas. Corrigido usando deteção de transição wasNotLoaded && nowLoaded em vez de flag one-shot.reloadFromBackup() novo em LibraryNotifier: recarrega favoritos/pastas excluídas/selecionadas do storage e chama rescan() — restauração completa sem reiniciar o app.ThemeNotifier.reloadFromStorage() e AudioSettingsNotifier.reloadFromStorage() tornados públicos para serem chamados externamente._BackupSheet convertido de StatefulWidget → ConsumerStatefulWidget para ter acesso ao ref e chamar os providers após a importação..json. O utilizador navega no armazenamento, escolhe o arquivo e a restauração ocorre.file_picker: ^8.1.4 com withData: true (lê os bytes via SAF, sem depender de path absoluto — funciona com Drive, Downloads, etc.) e fallback para leitura por path quando os bytes não vêm preenchidos.allowMalformed: true e tratamento de erros com SnackBar específico para arquivo vazio/ilegível vs. formato inválido.PageView disparam onPageChanged sem intervenção do utilizador. O guard _syncing antigo não era robusto o suficiente — em alguns timings, o evento atrasado caía no caminho de “swipe real” e chamava playIndex(idx)/notifier.next()/previous(), reiniciando a faixa (sem shuffle) ou saltando para uma aleatória (com shuffle).PageView com um Listener que regista o timestamp do último PointerDown. O _onPageChanged só trata o evento como swipe real quando houve toque nos últimos 800 ms; caso contrário, apenas atualiza _currentPage sem chamar nenhuma navegação. Soluciona ambos os sintomas de forma definitiva.BackupService.importFromPath(path): lê o JSON diretamente do disco, validando existência do arquivo (retorna -1 quando o caminho é inválido) e reaproveitando o pipeline de importFromString./storage/emulated/0/Download/constanza-backup.json) e mensagens de erro específicas para arquivo inexistente vs. formato inválido.dart:io File já disponível via backup_service.dart.flutter analyze em 0 issues.AndroidManifest: removido android:requestLegacyExternalStorage (MediaTagPlugin já usa MediaStore/ContentResolver).main.dart: silenciamento de debugPrint em kReleaseMode cobre 84 chamadas em 22 arquivos com uma única linha.library_provider: dev.log migrado para debugPrint (entra no mesmo pipeline).dispose() em 12 arquivos com AnimationController/StreamSubscription/Timer — todos OK, sem leaks.signingConfig condicional via android/key.properties (fora do git); fallback para chave de debug em sideload.audio_service, just_audio/ExoPlayer, JAudioTagger e plugins nativos.key.properties.example com instruções de keytool; .gitignore com padrões *.jks, *.keystore, key.properties.now_playing_page.dartpart of (preserva privacidade _Class, zero churn em imports).now_playing/styles/: 10 arquivos (classic, circular, large, full_blur, vinyl, minimalist, aurora, elegant, wave, mosaic).now_playing/lyrics/: 7 arquivos (lyrics_page, mini_player, view_mode, landscape, quick_sync, edit_view, empty).just_audio 0.9.43 → 0.10.5permission_handler 11.3.1 → 12.0.1share_plus 10.0.0 → 11.1.0 (v12+ exige AGP ≥8.12.1 + Kotlin 2.2.0; v11 mantém mesma API SharePlus.instance)cupertino_icons adicionado para resolver warning de fontes no build.ignore_for_file: deprecated_member_use removido dos 4 arquivos restantes; 6 deprecations corrigidas (Switch.activeColor → activeThumbColor, Color.value → toARGB32()).MediaTagPlugin: validação de path traversal + whitelist de extensões de áudio antes de qualquer writeTags/deleteSong (defesa em profundidade na fronteira nativa).BackupService exporta playlists, favoritos, exclusões, histórico, settings de tema/áudio/biblioteca como JSON via Share; importação por área de transferência. android:allowBackup + backup_rules.xml + data_extraction_rules.xml para Auto Backup nativo.Permission.ignoreBatteryOptimizations).launch_background.xml), em vez de fundo preto puro.MusicWidgetProvider) — artwork grande esquerda + título/artista + controlos completos (repeat/prev/play/next/star)WidgetLargeProvider) — artwork quase full-width + título/artista + controlos completosWidgetMediumSearchProvider) — linha compacta com barra de pesquisa que abre a appWidgetCapsuleProvider) — formato pílula com artwork, título, play/pause e nextWidgetRecommendationProvider) — artwork de fundo com overlay escuro e label “Daily Recommendation”WidgetRecommendedProvider) — artwork + transporte + grelha 2×2 de ações (fila, playlist, timer, favorito)WidgetConstants.kt — chaves SharedPreferences e nomes de ações centralizadosBaseWidgetProvider.kt — lógica partilhada (leitura de prefs, artwork arredondado, PendingIntents, abertura da app)MusicWidgetProvider.kt refatorado para usar BaseWidgetProviderMainActivity.kt notifica todos os 6 providers em cada updateWidgetAndroidManifest.xml regista os 6 receivers com respetivos appwidget-provider info XMLsres/layout/ com fundo vinho escuro (#E0521212) e bordas arredondadasPopScope em AppShell)./folders): dois níveis de controlo:
lib/presentation/widgets/now_playing_card.dart): card moderno de reprodução reutilizável com gradiente dinâmico da palette da artwork, barra de progresso inline, controles prev/play/next e animação de entrada; pode ser integrado em qualquer página.share_plus.SettingsStorageService: novo campo constanza_excluded_songs (lista de IDs persistida).LibraryState: novo campo excludedSongIds: Set<String> propagado no estado reativo.LibraryNotifier: novo método setExcludedSongs(ids), getter allScannedSongs, e _applyFolderFilter actualizado para aplicar a blacklist de IDs.share_plus: ^10.0.0 — partilha de ficheiros via intent nativo AndroidAudioPlayer criado com handleInterruptions: false e handleAudioSessionActivation: falseAudioSession configurada com mixWithOthers (iOS) e androidWillPauseWhenDucked: false