From 76f30a0f20f2b09680d27525cde3d1c9617fad5a Mon Sep 17 00:00:00 2001 From: Lobo <88998991+Lobooooooo14@users.noreply.github.com> Date: Wed, 23 Aug 2023 12:52:34 -0300 Subject: [PATCH 01/15] feat(translations): added Portuguese (Brazil) translation (#634) * chore: update library credits * chore: increase score when title matchs * chore: adjust score for both official flag & track name in title * chore: use scoring for non-english tracks only * added Portuguese (Brazil) translation --------- Co-authored-by: Kingkor Roy Tirtho --- README.md | 34 ++-- bin/gen-credits.dart | 4 +- lib/collections/language_codes.dart | 8 +- lib/l10n/app_pt.arb | 260 ++++++++++++++++++++++++++++ lib/l10n/l10n.dart | 1 + lib/models/spotube_track.dart | 84 +++++---- lib/utils/service_utils.dart | 7 + 7 files changed, 339 insertions(+), 59 deletions(-) create mode 100644 lib/l10n/app_pt.arb diff --git a/README.md b/README.md index 0a7849c6..a6d8433b 100644 --- a/README.md +++ b/README.md @@ -203,13 +203,20 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [collection](https://pub.dev/packages/collection) - Collections and utilities functions and classes related to collections. 1. [cupertino_icons](https://pub.dev/packages/cupertino_icons) - Default icons asset for Cupertino widgets based on Apple styled icons 1. [curved_navigation_bar](https://github.com/rafalbednarczuk/curved_navigation_bar) - Stunning Animating Curved Shape Navigation Bar. Adjustable color, background color, animation curve, animation duration. +1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop. +1. [device_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on. +1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device. +1. [dio](https://github.com/cfug/dio) - A powerful HTTP networking package,supports Interceptors,Aborting and canceling a request,Custom adapters, Transformers, etc. +1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc +1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration. 1. [envied](https://github.com/petercinibulk/envied) - Explicitly reads environment variables into a dart file from a .env file for more security and faster start up times. 1. [file_picker](https://github.com/miguelpruivo/plugins_flutter_file_picker) - A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support. 1. [fl_query](https://fl-query.vercel.app) - Asynchronous data caching, refetching & invalidation library for Flutter 1. [fl_query_hooks](https://fl-query.vercel.app) - Elite flutter_hooks compatible library for fl_query, the Asynchronous data caching, refetching & invalidation library for Flutter 1. [fl_query_connectivity_plus_adapter](https://fl-query.vercel.app) - Connectivity Plus adapter for FlQuery Connectivity 1. [fluentui_system_icons](https://github.com/microsoft/fluentui-system-icons/tree/main) - Fluent UI System Icons are a collection of familiar, friendly and modern icons from Microsoft. -1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/master/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. +1. [flutter_cache_manager](https://github.com/Baseflow/flutter_cache_manager/tree/develop/flutter_cache_manager) - Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. +1. [flutter_displaymode](https://github.com/ajinasokan/flutter_displaymode) - A Flutter plugin to set display mode (resolution, refresh rate) on Android platform. Allows to enable high refresh rate on supported devices. 1. [flutter_feather_icons](https://github.com/muj-programmer/flutter_feather_icons) - Feather is a collection of simply beautiful open source icons. Each icon is designed on a 24x24 grid with an emphasis on simplicity, consistency and usability. 1. [flutter_hooks](https://github.com/rrousselGit/flutter_hooks) - A flutter implementation of React hooks. It adds a new kind of widget with enhanced code reuse. 1. [flutter_inappwebview](https://inappwebview.dev/) - A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window. @@ -218,6 +225,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) - Flutter Secure Storage provides API to store data in secure storage. Keychain is used in iOS, KeyStore based solution is used in Android. 1. [flutter_svg](https://pub.dev/packages/flutter_svg) - An SVG rendering and widget library for Flutter, which allows painting and displaying Scalable Vector Graphics 1.1 files. 1. [fuzzywuzzy](https://github.com/sphericalkat/dart-fuzzywuzzy) - An implementation of the popular fuzzywuzzy package in Dart, to suit all your fuzzy string matching/searching needs! +1. [google_fonts](https://pub.dev/packages/google_fonts) - A Flutter package to use fonts from fonts.google.com. Supports HTTP fetching, caching, and asset bundling. 1. [go_router](https://pub.dev/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more 1. [hive](https://github.com/hivedb/hive/tree/master/hive) - Lightweight and blazing fast key-value database written in pure Dart. Strongly encrypted using AES-256. 1. [hive_flutter](https://github.com/hivedb/hive/tree/master/hive_flutter) - Extension for Hive. Makes it easier to use Hive in Flutter apps. @@ -228,12 +236,13 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [introduction_screen](https://github.com/pyozer/introduction_screen) - Introduction/Onboarding package for flutter app with some customizations possibilities 1. [json_annotation](https://pub.dev/packages/json_annotation) - Classes and helper functions that support JSON code generation via the `json_serializable` package. 1. [logger](https://pub.dev/packages/logger) - Small, easy to use and extensible logger which prints beautiful logs. -1. [media_kit](https://github.com/alexmercerind/media_kit) - A complete video & audio playback library for Flutter & Dart. Performant, stable, feature-proof & modular. -1. [media_kit_libs_android_audio](https://github.com/alexmercerind/media_kit.git) - Android package providing audio (only) native libraries for package:media_kit. -1. [media_kit_libs_ios_audio](https://github.com/alexmercerind/media_kit.git) - iOS package providing audio native libraries for package:media_kit. -1. [media_kit_libs_linux](https://github.com/alexmercerind/media_kit.git) - GNU/Linux dependency package for package:media_kit. Necessary for initialization. -1. [media_kit_libs_macos_audio](https://github.com/alexmercerind/media_kit.git) - macOS package providing audio native libraries for package:media_kit. -1. [media_kit_libs_windows_audio](https://github.com/alexmercerind/media_kit.git) - Windows package providing audio (only) native libraries for package:media_kit. +1. [media_kit](https://github.com/media-kit/media-kit) - A cross-platform video player & audio player for Flutter & Dart. Performant, stable, feature-proof & modular. +1. [media_kit_native_event_loop](https://github.com/media-kit/media-kit) - Platform specific threaded event handling for media_kit. Enables support for higher number of concurrent instances. +1. [media_kit_libs_android_audio](https://github.com/media-kit/media-kit.git) - Android package providing audio (only) native libraries for package:media_kit. +1. [media_kit_libs_ios_audio](https://github.com/media-kit/media-kit.git) - iOS package providing audio native libraries for package:media_kit. +1. [media_kit_libs_macos_audio](https://github.com/media-kit/media-kit.git) - macOS package providing audio native libraries for package:media_kit. +1. [media_kit_libs_windows_audio](https://github.com/media-kit/media-kit.git) - Windows package providing audio (only) native libraries for package:media_kit. +1. [media_kit_libs_linux](https://github.com/media-kit/media-kit.git) - GNU/Linux dependency package for package:media_kit. Necessary for initialization. 1. [metadata_god](https://github.com/KRTirtho/metadata_god) - Plugin for retrieving and writing audio tags/metadata from audio files 1. [mime](https://pub.dev/packages/mime) - Utilities for handling media (MIME) types, including determining a type from a file extension and file contents. 1. [package_info_plus](https://plus.fluttercommunity.dev/) - Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. @@ -241,6 +250,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [path](https://pub.dev/packages/path) - A string-based path manipulation library. All of the path operations you know and love, with solid support for Windows, POSIX (Linux and Mac OS X), and the web. 1. [path_provider](https://pub.dev/packages/path_provider) - Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. 1. [permission_handler](https://pub.dev/packages/permission_handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. +1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video 1. [popover](https://github.com/minikin/popover) - A popover is a transient view that appears above other content onscreen when you tap a control or in an area. 1. [scroll_to_index](https://github.com/quire-io/scroll-to-index) - Scroll to a specific child of any scrollable widget in Flutter 1. [shared_preferences](https://pub.dev/packages/shared_preferences) - Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. @@ -248,6 +258,7 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [skeleton_text](https://github.com/101Loop/Skeleton-Text) - A package that provides an easy way to add skeleton text loading animation in Flutter project. This project is a part of 101Loop community. 1. [smtc_windows](https://github.com/KRTirtho/smtc_windows) - Windows `SystemMediaTransportControls` implementation for Flutter giving access to Windows OS Media Control applet. 1. [spotify](https://github.com/rinukkusu/spotify-dart) - An incomplete dart library for interfacing with the Spotify Web API. +1. [supabase](https://supabase.com) - A dart client for Supabase. This client makes it simple for developers to build secure and scalable products. 1. [system_theme](https://pub.dev/packages/system_theme) - A plugin to get the current system theme info. Supports Android, Web, Windows, Linux and macOS 1. [titlebar_buttons](https://github.com/gtk-flutter/titlebar_buttons) - A package which provides most of the titlebar buttons from windows, linux and macos. 1. [url_launcher](https://pub.dev/packages/url_launcher) - Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. @@ -255,14 +266,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [version](https://github.com/dartninja/version) - Provides a simple class for parsing and comparing semantic versions as defined by http://semver.org/ 1. [visibility_detector](https://pub.dev/packages/visibility_detector) - A widget that detects the visibility of its child and notifies a callback. 1. [window_manager](https://github.com/leanflutter/window_manager) - This plugin allows Flutter desktop apps to resizing and repositioning the window. -1. [piped_client](https://github.com/KRTirtho/piped_client) - API Client for piped.video -1. [supabase_flutter](https://supabase.com) - Flutter integration for Supabase. This package makes it simple for developers to build secure and scalable products. -1. [device_preview](https://github.com/aloisdeniel/flutter_device_preview) - Approximate how your Flutter app looks and performs on another device. -1. [media_kit_native_event_loop](https://github.com/alexmercerind/media_kit) - Platform specific threaded event handling for media_kit. Enables support for higher number of concurrent instances. -1. [dbus](https://github.com/canonical/dbus.dart) - A native Dart implementation of the D-Bus message bus client. This package allows Dart applications to directly access services on the Linux desktop. -1. [background_downloader](https://pub.dev/packages/background_downloader) - A multi-platform background file downloader and uploader. Define the task, enqueue and monitor progress -1. [duration](https://github.com/desktop-dart/duration) - Utilities to make working with 'Duration's easier. Formats duration in human readable form and also parses duration in human readable form to Dart's Duration. -1. [disable_battery_optimization](https://github.com/pvsvamsi/Disable-Battery-Optimizations) - Flutter plugin to check and disable battery optimizations. Also shows custom steps to disable the optimizations in devices like mi, xiaomi, samsung, oppo, huawei, oneplus etc 1. [youtube_explode_dart](https://github.com/Hexer10/youtube_explode_dart) - A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key. 1. [build_runner](https://pub.dev/packages/build_runner) - A build system for Dart code generation and modular compilation. 1. [envied_generator](https://github.com/petercinibulk/envied) - Generator for the Envied package. See https://pub.dev/packages/envied. @@ -277,7 +280,6 @@ If you are concerned, you can [read the reason of choosing this license](https:/ 1. [catcher](https://github.com/jhomlala/catcher) - Plugin for error catching which provides multiple handlers for dealing with errors when they are not caught by the developer. 1. [flutter_desktop_tools](https://github.com/KRTirtho/flutter_desktop_tools) - Essential collection of tools for flutter desktop app development 1. [window_size](https://github.com/google/flutter-desktop-embedding.git) - Allows resizing and repositioning the window containing Flutter. -

© Copyright Spotube 2023

diff --git a/bin/gen-credits.dart b/bin/gen-credits.dart index 74f4a935..6f3e1b51 100644 --- a/bin/gen-credits.dart +++ b/bin/gen-credits.dart @@ -72,7 +72,7 @@ void main() async { packageInfo .map( (package) => - '- [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}', + '1. [${package.name}](${package.latestPubspec.homepage ?? package.url}) - ${package.description.replaceAll('\n', '')}', ) .join('\n'), ); @@ -85,7 +85,7 @@ void main() async { ?.value .url .toString(); - return '- [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}'; + return '1. [${package.name}]($packageUrl) - ${package.description?.replaceAll('\n', '')}'; }, ).join('\n'), ); diff --git a/lib/collections/language_codes.dart b/lib/collections/language_codes.dart index 8209f33d..cdb6a76d 100644 --- a/lib/collections/language_codes.dart +++ b/lib/collections/language_codes.dart @@ -520,10 +520,10 @@ abstract class LanguageLocals { // name: "Pashto, Pushto", // nativeName: "پښتو", // ), - // "pt": const ISOLanguageName( - // name: "Portuguese", - // nativeName: "Português", - // ), + "pt": const ISOLanguageName( + name: "Portuguese", + nativeName: "Português", + ), // "qu": const ISOLanguageName( // name: "Quechua", // nativeName: "Runa Simi, Kichwa", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb new file mode 100644 index 00000000..af26c335 --- /dev/null +++ b/lib/l10n/app_pt.arb @@ -0,0 +1,260 @@ +{ + "guest": "Visitante", + "browse": "Explorar", + "search": "Buscar", + "library": "Biblioteca", + "lyrics": "Letras", + "settings": "Configurações", + "genre_categories_filter": "Filtrar categorias ou gêneros...", + "genre": "Gênero", + "personalized": "Personalizado", + "featured": "Destaque", + "new_releases": "Novos Lançamentos", + "songs": "Músicas", + "playing_track": "Tocando {track}", + "queue_clear_alert": "Isso irá limpar a fila atual. {track_length} músicas serão removidas.\nDeseja continuar?", + "load_more": "Carregar mais", + "playlists": "Playlists", + "artists": "Artistas", + "albums": "Álbuns", + "tracks": "Faixas", + "downloads": "Downloads", + "filter_playlists": "Filtrar suas playlists...", + "liked_tracks": "Músicas Curtidas", + "liked_tracks_description": "Todas as suas músicas curtidas", + "create_playlist": "Criar Playlist", + "create_a_playlist": "Criar uma playlist", + "create": "Criar", + "cancel": "Cancelar", + "playlist_name": "Nome da Playlist", + "name_of_playlist": "Nome da playlist", + "description": "Descrição", + "public": "Pública", + "collaborative": "Colaborativa", + "search_local_tracks": "Buscar músicas locais...", + "play": "Reproduzir", + "delete": "Excluir", + "none": "Nenhum", + "sort_a_z": "Ordenar de A-Z", + "sort_z_a": "Ordenar de Z-A", + "sort_artist": "Ordenar por Artista", + "sort_album": "Ordenar por Álbum", + "sort_tracks": "Ordenar Faixas", + "currently_downloading": "Baixando no momento ({tracks_length})", + "cancel_all": "Cancelar Tudo", + "filter_artist": "Filtrar artistas...", + "followers": "{followers} Seguidores", + "add_artist_to_blacklist": "Adicionar artista à lista negra", + "top_tracks": "Principais Músicas", + "fans_also_like": "Fãs também curtiram", + "loading": "Carregando...", + "artist": "Artista", + "blacklisted": "Na Lista Negra", + "following": "Seguindo", + "follow": "Seguir", + "artist_url_copied": "URL do artista copiada para a área de transferência", + "added_to_queue": "Adicionadas {tracks} músicas à fila", + "filter_albums": "Filtrar álbuns...", + "synced": "Sincronizado", + "plain": "Simples", + "shuffle": "Aleatório", + "search_tracks": "Buscar músicas...", + "released": "Lançado", + "error": "Erro {error}", + "title": "Título", + "time": "Tempo", + "more_actions": "Mais ações", + "download_count": "Baixar ({count})", + "add_count_to_playlist": "Adicionar ({count}) à Playlist", + "add_count_to_queue": "Adicionar ({count}) à Fila", + "play_count_next": "Reproduzir ({count}) em seguida", + "album": "Álbum", + "copied_to_clipboard": "{data} copiado para a área de transferência", + "add_to_following_playlists": "Adicionar {track} às Playlists Seguintes", + "add": "Adicionar", + "added_track_to_queue": "Adicionada {track} à fila", + "add_to_queue": "Adicionar à fila", + "track_will_play_next": "{track} será reproduzida em seguida", + "play_next": "Reproduzir em seguida", + "removed_track_from_queue": "{track} removida da fila", + "remove_from_queue": "Remover da fila", + "remove_from_favorites": "Remover dos favoritos", + "save_as_favorite": "Salvar como favorita", + "add_to_playlist": "Adicionar à playlist", + "remove_from_playlist": "Remover da playlist", + "add_to_blacklist": "Adicionar à lista negra", + "remove_from_blacklist": "Remover da lista negra", + "share": "Compartilhar", + "mini_player": "Mini Player", + "slide_to_seek": "Arraste para avançar ou retroceder", + "shuffle_playlist": "Embaralhar playlist", + "unshuffle_playlist": "Desembaralhar playlist", + "previous_track": "Faixa anterior", + "next_track": "Próxima faixa", + "pause_playback": "Pausar Reprodução", + "resume_playback": "Continuar Reprodução", + "loop_track": "Repetir faixa", + "repeat_playlist": "Repetir playlist", + "queue": "Fila", + "alternative_track_sources": "Fontes alternativas de faixas", + "download_track": "Baixar faixa", + "tracks_in_queue": "{tracks} músicas na fila", + "clear_all": "Limpar tudo", + "show_hide_ui_on_hover": "Mostrar/Ocultar UI ao passar o mouse", + "always_on_top": "Sempre no topo", + "exit_mini_player": "Sair do Mini player", + "download_location": "Local de download", + "account": "Conta", + "login_with_spotify": "Fazer login com sua conta do Spotify", + "connect_with_spotify": "Conectar ao Spotify", + "logout": "Sair", + "logout_of_this_account": "Sair desta conta", + "language_region": "Idioma e Região", + "language": "Idioma", + "system_default": "Padrão do Sistema", + "market_place_region": "Região da Loja", + "recommendation_country": "País de Recomendação", + "appearance": "Aparência", + "layout_mode": "Modo de Layout", + "override_layout_settings": "Substituir configurações do modo de layout responsivo", + "adaptive": "Adaptável", + "compact": "Compacto", + "extended": "Estendido", + "theme": "Tema", + "dark": "Escuro", + "light": "Claro", + "system": "Sistema", + "accent_color": "Cor de Destaque", + "sync_album_color": "Sincronizar cor do álbum", + "sync_album_color_description": "Usa a cor predominante da capa do álbum como cor de destaque", + "playback": "Reprodução", + "audio_quality": "Qualidade do Áudio", + "high": "Alta", + "low": "Baixa", + "pre_download_play": "Pré-download e reprodução", + "pre_download_play_description": "Em vez de transmitir áudio, baixar bytes e reproduzir (recomendado para usuários com maior largura de banda)", + "skip_non_music": "Pular segmentos não musicais (SponsorBlock)", + "blacklist_description": "Faixas e artistas na lista negra", + "wait_for_download_to_finish": "Aguarde o download atual ser concluído", + "download_lyrics": "Baixar letras junto com as faixas", + "desktop": "Desktop", + "close_behavior": "Comportamento de Fechamento", + "close": "Fechar", + "minimize_to_tray": "Minimizar para a bandeja", + "show_tray_icon": "Mostrar ícone na bandeja do sistema", + "about": "Sobre", + "u_love_spotube": "Sabemos que você adora o Spotube", + "check_for_updates": "Verificar atualizações", + "about_spotube": "Sobre o Spotube", + "blacklist": "Lista Negra", + "please_sponsor": "Por favor, patrocine/doe", + "spotube_description": "Spotube, um cliente leve, multiplataforma e gratuito para o Spotify", + "version": "Versão", + "build_number": "Número de Build", + "founder": "Fundador", + "repository": "Repositório", + "bug_issues": "Bugs/Problemas", + "made_with": "Feito com ❤️ em Bangladesh🇧🇩", + "kingkor_roy_tirtho": "Kingkor Roy Tirtho", + "copyright": "© 2021-{current_year} Kingkor Roy Tirtho", + "license": "Licença", + "add_spotify_credentials": "Adicione suas credenciais do Spotify para começar", + "credentials_will_not_be_shared_disclaimer": "Não se preocupe, suas credenciais não serão coletadas nem compartilhadas com ninguém", + "know_how_to_login": "Não sabe como fazer isso?", + "follow_step_by_step_guide": "Siga o guia passo a passo", + "spotify_cookie": "Cookie do Spotify {name}", + "cookie_name_cookie": "Cookie {name}", + "fill_in_all_fields": "Preencha todos os campos, por favor", + "submit": "Enviar", + "exit": "Sair", + "previous": "Anterior", + "next": "Próximo", + "done": "Concluído", + "step_1": "Passo 1", + "first_go_to": "Primeiro, vá para", + "login_if_not_logged_in": "e faça login/cadastro se ainda não estiver logado", + "step_2": "Passo 2", + "step_2_steps": "1. Uma vez logado, pressione F12 ou clique com o botão direito do mouse > Inspecionar para abrir as ferramentas de desenvolvimento do navegador.\n2. Em seguida, vá para a guia \"Aplicativo\" (Chrome, Edge, Brave, etc.) ou \"Armazenamento\" (Firefox, Palemoon, etc.)\n3. Acesse a seção \"Cookies\" e depois a subseção \"https://accounts.spotify.com\"", + "step_3": "Passo 3", + "step_3_steps": "Copie os valores dos Cookies \"sp_dc\" e \"sp_key\" (ou sp_gaid)", + "success_emoji": "Sucesso🥳", + "success_message": "Agora você está logado com sucesso em sua conta do Spotify. Bom trabalho!", + "step_4": "Passo 4", + "step_4_steps": "Cole os valores copiados \"sp_dc\" e \"sp_key\" (ou sp_gaid) nos campos correspondentes", + "something_went_wrong": "Algo deu errado", + "piped_instance": "Instância do Servidor Piped", + "piped_description": "A instância do servidor Piped a ser usada para correspondência de faixas", + "piped_warning": "Algumas delas podem não funcionar bem. Use por sua conta e risco", + "generate_playlist": "Gerar Playlist", + "track_exists": "A faixa {track} já existe", + "replace_downloaded_tracks": "Substituir todas as faixas baixadas", + "skip_download_tracks": "Pular o download de todas as faixas baixadas", + "do_you_want_to_replace": "Deseja substituir a faixa existente?", + "replace": "Substituir", + "skip": "Pular", + "select_up_to_count_type": "Selecione até {count} {type}", + "select_genres": "Selecionar Gêneros", + "add_genres": "Adicionar Gêneros", + "country": "País", + "number_of_tracks_generate": "Número de faixas a gerar", + "acousticness": "Acústica", + "danceability": "Dançabilidade", + "energy": "Energia", + "instrumentalness": "Instrumentalidade", + "liveness": "Vivacidade", + "loudness": "Volume", + "speechiness": "Discurso", + "valence": "Valência", + "popularity": "Popularidade", + "key": "Tonalidade", + "duration": "Duração (s)", + "tempo": "Tempo (BPM)", + "mode": "Modo", + "time_signature": "Assinatura de tempo", + "short": "Curto", + "medium": "Médio", + "long": "Longo", + "min": "Min", + "max": "Máx", + "target": "Alvo", + "moderate": "Moderado", + "deselect_all": "Desmarcar Todos", + "select_all": "Selecionar Todos", + "are_you_sure": "Tem certeza?", + "generating_playlist": "Gerando sua playlist personalizada...", + "selected_count_tracks": "{count} faixas selecionadas", + "download_warning": "Se você baixar todas as faixas em massa, estará claramente pirateando música e causando danos à sociedade criativa da música. Espero que você esteja ciente disso. Sempre tente respeitar e apoiar o trabalho árduo dos artistas", + "download_ip_ban_warning": "Além disso, seu IP pode ser bloqueado no YouTube devido a solicitações de download excessivas. O bloqueio de IP significa que você não poderá usar o YouTube (mesmo se estiver conectado) por pelo menos 2-3 meses a partir do dispositivo IP. E o Spotube não se responsabiliza se isso acontecer", + "by_clicking_accept_terms": "Ao clicar em 'aceitar', você concorda com os seguintes termos:", + "download_agreement_1": "Eu sei que estou pirateando música. Sou mau", + "download_agreement_2": "Vou apoiar o artista onde puder e estou fazendo isso porque não tenho dinheiro para comprar sua arte", + "download_agreement_3": "Estou completamente ciente de que meu IP pode ser bloqueado no YouTube e não responsabilizo o Spotube ou seus proprietários/colaboradores por quaisquer acidentes causados pela minha ação atual", + "decline": "Recusar", + "accept": "Aceitar", + "details": "Detalhes", + "youtube": "YouTube", + "channel": "Canal", + "likes": "Curtidas", + "dislikes": "Descurtidas", + "views": "Visualizações", + "streamUrl": "URL do Stream", + "stop": "Parar", + "sort_newest": "Ordenar por mais recente adicionado", + "sort_oldest": "Ordenar por mais antigo adicionado", + "sleep_timer": "Temporizador de Sono", + "mins": "{minutes} Minutos", + "hours": "{hours} Horas", + "hour": "{hours} Hora", + "custom_hours": "Horas Personalizadas", + "logs": "Registros", + "developers": "Desenvolvedores", + "not_logged_in": "Você não está logado", + "search_mode": "Modo de Busca", + "youtube_api_type": "Tipo de API", + "ok": "Ok", + "failed_to_encrypt": "Falha ao criptografar", + "encryption_failed_warning": "O Spotube usa criptografia para armazenar seus dados com segurança, mas falhou em fazê-lo. Portanto, ele voltará para o armazenamento não seguro.\nSe você estiver usando o Linux, certifique-se de ter algum serviço secreto (gnome-keyring, kde-wallet, keepassxc, etc.) instalado", + "querying_info": "Consultando informações...", + "piped_api_down": "A API do Piped está fora do ar", + "piped_down_error_instructions": "A instância do Piped {pipedInstance} está fora do ar no momento\n\nMude a instância ou altere o 'Tipo de API' para a API oficial do YouTube\n\nCertifique-se de reiniciar o aplicativo após a alteração" +} diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 449cf5fb..f1efdb75 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -19,5 +19,6 @@ class L10n { const Locale('ja', 'JP'), const Locale('zh', 'CN'), const Locale('pl', 'PL'), + const Locale('pt', 'PT'), ]; } diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 6b5e9b64..268a273e 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -10,7 +10,7 @@ import 'package:spotube/utils/service_utils.dart'; import 'package:collection/collection.dart'; final officialMusicRegex = RegExp( - r"official\s(video|audio|music\svideo)", + r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", caseSensitive: false, ); @@ -68,54 +68,64 @@ class SpotubeTrack extends Track { onlyCleanArtist: true, ).trim(); - final List siblings = - await client.search("$title - ${artists.join(", ")}").then( + final query = "$title - ${artists.join(", ")}"; + final List siblings = await client.search(query).then( (res) { final isYoutubeApi = client.preferences.youtubeApiType == YoutubeApiType.youtube; final siblings = isYoutubeApi || client.preferences.searchMode == SearchMode.youtube - ? res - .sorted((a, b) => b.views.compareTo(a.views)) - .map((sibling) { - int score = 0; + ? ServiceUtils.onlyContainsEnglish(query) + ? res + : res + .sorted((a, b) => b.views.compareTo(a.views)) + .map((sibling) { + int score = 0; - for (final artist in artists) { - final isSameChannelArtist = - sibling.channelName.toLowerCase() == - artist.toLowerCase(); - final channelContainsArtist = sibling.channelName - .toLowerCase() - .contains(artist.toLowerCase()); + for (final artist in artists) { + final isSameChannelArtist = + sibling.channelName.toLowerCase() == + artist.toLowerCase(); + final channelContainsArtist = sibling.channelName + .toLowerCase() + .contains(artist.toLowerCase()); - if (isSameChannelArtist || channelContainsArtist) { - score += 1; - } + if (isSameChannelArtist || channelContainsArtist) { + score += 1; + } - final titleContainsArtist = sibling.title - .toLowerCase() - .contains(artist.toLowerCase()); + final titleContainsArtist = sibling.title + .toLowerCase() + .contains(artist.toLowerCase()); - if (titleContainsArtist) { - score += 1; - } - } + if (titleContainsArtist) { + score += 1; + } + } - if (sibling.title - .toLowerCase() - .contains(track.name!.toLowerCase())) { - score += 2; - } + final titleContainsTrackName = sibling.title + .toLowerCase() + .contains(track.name!.toLowerCase()); - if (officialMusicRegex - .hasMatch(sibling.title.toLowerCase())) { - score += 1; - } + final hasOfficialFlag = officialMusicRegex + .hasMatch(sibling.title.toLowerCase()); - return (sibling: sibling, score: score); - }) - .sorted((a, b) => b.score.compareTo(a.score)) - .map((e) => e.sibling) + if (titleContainsTrackName) { + score += 3; + } + + if (hasOfficialFlag) { + score += 1; + } + + if (hasOfficialFlag && titleContainsTrackName) { + score += 2; + } + + return (sibling: sibling, score: score); + }) + .sorted((a, b) => b.score.compareTo(a.score)) + .map((e) => e.sibling) : res.sorted((a, b) => b.views.compareTo(a.views)).where((item) { return artists.any( (artist) => diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 64aa45b6..2d44b984 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -17,6 +17,13 @@ import 'package:html/parser.dart' as parser; abstract class ServiceUtils { static final logger = getLogger("ServiceUtils"); + static final _englishMatcherRegex = RegExp( + "^[a-zA-Z0-9\\s!\"#\$%&\\'()*+,-.\\/:;<=>?@\\[\\]^_`{|}~]*\$", + ); + static bool onlyContainsEnglish(String text) { + return _englishMatcherRegex.hasMatch(text); + } + static String clearArtistsOfTitle(String title, List artists) { return title .replaceAll(RegExp(artists.join("|"), caseSensitive: false), "") From 6ced0a0fad06f9f431636ca0fe5dae83eafe33ce Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Aug 2023 18:05:18 +0600 Subject: [PATCH 02/15] fix: always fetching SponsorBlock if no segments found & download failing --- .../dialogs/confirm_download_dialog.dart | 6 +- .../shared/track_table/tracks_table_view.dart | 18 +++-- lib/models/spotube_track.dart | 8 ++- lib/provider/download_manager_provider.dart | 2 +- .../proxy_playlist_provider.dart | 69 ++++++++++++------- lib/provider/youtube_provider.dart | 10 --- .../audio_services/windows_audio_service.dart | 3 +- 7 files changed, 67 insertions(+), 49 deletions(-) diff --git a/lib/components/shared/dialogs/confirm_download_dialog.dart b/lib/components/shared/dialogs/confirm_download_dialog.dart index 0413260c..c371e803 100644 --- a/lib/components/shared/dialogs/confirm_download_dialog.dart +++ b/lib/components/shared/dialogs/confirm_download_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; class ConfirmDownloadDialog extends StatelessWidget { @@ -24,8 +25,9 @@ class ConfirmDownloadDialog extends StatelessWidget { ], ), ), - content: Padding( + content: Container( padding: const EdgeInsets.all(15), + constraints: BoxConstraints(maxWidth: Breakpoints.sm), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, @@ -87,7 +89,7 @@ class BulletPoint extends StatelessWidget { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("●"), + const Text("\u2022"), const SizedBox(width: 5), Flexible(child: Text(text)), ], diff --git a/lib/components/shared/track_table/tracks_table_view.dart b/lib/components/shared/track_table/tracks_table_view.dart index d412dd36..58d662f4 100644 --- a/lib/components/shared/track_table/tracks_table_view.dart +++ b/lib/components/shared/track_table/tracks_table_view.dart @@ -20,6 +20,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/utils/service_utils.dart'; final trackCollectionSortState = @@ -55,7 +56,9 @@ class TracksTableView extends HookConsumerWidget { final playback = ref.watch(ProxyPlaylistNotifier.notifier); ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider.notifier); - TextStyle tableHeadStyle = + final apiType = + ref.watch(userPreferencesProvider.select((s) => s.youtubeApiType)); + final tableHeadStyle = const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); final selected = useState>([]); @@ -188,12 +191,13 @@ class TracksTableView extends HookConsumerWidget { switch (action) { case "download": { - final confirmed = await showDialog( - context: context, - builder: (context) { - return const ConfirmDownloadDialog(); - }, - ); + final confirmed = apiType == YoutubeApiType.piped || + await showDialog( + context: context, + builder: (context) { + return const ConfirmDownloadDialog(); + }, + ); if (confirmed != true) return; await downloader .batchAddToQueue(selectedTracks.toList()); diff --git a/lib/models/spotube_track.dart b/lib/models/spotube_track.dart index 268a273e..6ef240df 100644 --- a/lib/models/spotube_track.dart +++ b/lib/models/spotube_track.dart @@ -14,6 +14,12 @@ final officialMusicRegex = RegExp( caseSensitive: false, ); +class TrackNotFoundException implements Exception { + factory TrackNotFoundException(Track track) { + throw Exception("Failed to find any results for ${track.name}"); + } +} + class SpotubeTrack extends Track { final YoutubeVideoInfo ytTrack; final String ytUri; @@ -157,7 +163,7 @@ class SpotubeTrack extends Track { } else { siblings = await fetchSiblings(track, client); if (siblings.isEmpty) { - throw Exception("Failed to find any results for ${track.name}"); + throw TrackNotFoundException(track); } (ytVideo, ytStreamUrl) = await client.video(siblings.first.id, siblings.first.searchMode); diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index db443082..b5c4a0e7 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -85,7 +85,7 @@ class DownloadManagerProvider extends ChangeNotifier { final Ref ref; - YoutubeEndpoints get yt => ref.read(downloadYoutubeProvider); + YoutubeEndpoints get yt => ref.read(youtubeProvider); String get downloadDirectory => ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index b5d42fb2..4879867b 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart'; import 'package:palette_generator/palette_generator.dart'; @@ -68,7 +69,12 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier () async { notificationService = await AudioServices.create(ref, this); - ({String source, List segments})? currentSegments; + // listeners state + final currentSegments = + // using source as unique id because alternative track source support + ObjectRef<({String source, List segments})?>(null); + final isPreSearching = ObjectRef(false); + final isFetchingSegments = ObjectRef(false); audioPlayer.activeSourceChangedStream.listen((newActiveSource) async { try { @@ -112,16 +118,14 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } }); - bool isPreSearching = false; - listenTo2Percent(int percent) async { - if (isPreSearching || + if (isPreSearching.value || audioPlayer.currentSource == null || audioPlayer.nextSource == null || isPlayable(audioPlayer.nextSource!)) return; try { - isPreSearching = true; + isPreSearching.value = true; final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; @@ -138,51 +142,64 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier ); } } catch (e, stackTrace) { + // Removing tracks that were not found to avoid queue interruption + // TODO: Add a flag to enable/disable skip not found tracks + if (e is TrackNotFoundException) { + final oldTrack = + mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + await removeTrack(oldTrack!.id!); + } Catcher.reportCheckedError(e, stackTrace); } finally { - isPreSearching = false; + isPreSearching.value = false; } } audioPlayer.percentCompletedStream(2).listen(listenTo2Percent); - bool isFetchingSegments = false; - audioPlayer.positionStream.listen((position) async { + if (state.activeTrack == null || state.activeTrack is LocalTrack) { + isFetchingSegments.value = false; + return; + } try { - if (state.activeTrack == null || state.activeTrack is LocalTrack) { - isFetchingSegments = false; - return; - } - // skipping in very first second breaks stream - if ((preferences.youtubeApiType == YoutubeApiType.piped && - preferences.searchMode == SearchMode.youtubeMusic) || - !preferences.skipNonMusic) return; + final isYTMusicMode = + preferences.youtubeApiType == YoutubeApiType.piped && + preferences.searchMode == SearchMode.youtubeMusic; - final notSameSegmentId = - currentSegments?.source != audioPlayer.currentSource; + if (isYTMusicMode || !preferences.skipNonMusic) return; - if (currentSegments == null || - (notSameSegmentId && !isFetchingSegments)) { - isFetchingSegments = true; + final isNotSameSegmentId = + currentSegments.value?.source != audioPlayer.currentSource; + + if (currentSegments.value == null || + (isNotSameSegmentId && !isFetchingSegments.value)) { + isFetchingSegments.value = true; try { - currentSegments = ( + currentSegments.value = ( source: audioPlayer.currentSource!, segments: await getAndCacheSkipSegments( (state.activeTrack as SpotubeTrack).ytTrack.id, ), ); + } catch (e) { + currentSegments.value = ( + source: audioPlayer.currentSource!, + segments: [], + ); } finally { - isFetchingSegments = false; + isFetchingSegments.value = false; } } - final (source: _, :segments) = currentSegments!; + final (source: _, :segments) = currentSegments.value!; + + // skipping in first 2 second breaks stream if (segments.isEmpty || position < const Duration(seconds: 3)) return; for (final segment in segments) { - if ((position.inSeconds >= segment.start && - position.inSeconds < segment.end)) { + if (position.inSeconds >= segment.start && + position.inSeconds < segment.end) { await audioPlayer.seek(Duration(seconds: segment.end)); } } diff --git a/lib/provider/youtube_provider.dart b/lib/provider/youtube_provider.dart index 20b5ba2b..0e7b7d0e 100644 --- a/lib/provider/youtube_provider.dart +++ b/lib/provider/youtube_provider.dart @@ -6,13 +6,3 @@ final youtubeProvider = Provider((ref) { final preferences = ref.watch(userPreferencesProvider); return YoutubeEndpoints(preferences); }); - -// this provider overrides the API provider to use piped.video for downloading -final downloadYoutubeProvider = Provider((ref) { - final preferences = ref.watch(userPreferencesProvider); - return YoutubeEndpoints( - preferences.copyWith( - youtubeApiType: YoutubeApiType.piped, - ), - ); -}); diff --git a/lib/services/audio_services/windows_audio_service.dart b/lib/services/audio_services/windows_audio_service.dart index 0cd8f9bb..4481140b 100644 --- a/lib/services/audio_services/windows_audio_service.dart +++ b/lib/services/audio_services/windows_audio_service.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:smtc_windows/smtc_windows.dart' - if (dart.library.html) 'package:spotube/services/audio_services/smtc_windows_web.dart'; +import 'package:smtc_windows/smtc_windows.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; From d239d641ff8f1b3edd64243994fd4a58cf71a5d3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Aug 2023 18:54:54 +0600 Subject: [PATCH 03/15] feat: paginated user albums --- lib/components/album/album_card.dart | 9 +- lib/components/library/user_albums.dart | 117 ++++++++++-------- lib/components/shared/heart_button.dart | 10 +- .../proxy_playlist_provider.dart | 8 +- lib/services/mutations/album.dart | 4 + lib/services/queries/album.dart | 16 ++- 6 files changed, 100 insertions(+), 64 deletions(-) diff --git a/lib/components/album/album_card.dart b/lib/components/album/album_card.dart index afb637a0..b946209a 100644 --- a/lib/components/album/album_card.dart +++ b/lib/components/album/album_card.dart @@ -50,8 +50,13 @@ class AlbumCard extends HookConsumerWidget { () => playlist.containsCollection(album.id!), [playlist, album.id], ); - final int marginH = - useBreakpointValue(xs: 10, sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); + + final marginH = useBreakpointValue( + xs: 10, + sm: 10, + md: 15, + others: 20, + ); final updating = useState(false); final spotify = ref.watch(spotifyProvider); diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index 014a84f6..8df34346 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -3,13 +3,14 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotify/spotify.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; +import 'package:spotube/components/shared/waypoint.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/hooks/use_breakpoint_value.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/services/queries/queries.dart'; @@ -23,76 +24,86 @@ class UserAlbums extends HookConsumerWidget { final auth = ref.watch(AuthenticationNotifier.provider); final albumsQuery = useQueries.album.ofMine(ref); - final spacing = useBreakpointValue( - xs: 0, - sm: 0, - others: 20, - ); + final controller = useScrollController(); final searchText = useState(''); + final allAlbums = useMemoized( + () => albumsQuery.pages + .expand((element) => element.items ?? []), + [albumsQuery.pages], + ); + final albums = useMemoized(() { if (searchText.value.isEmpty) { - return albumsQuery.data?.toList() ?? []; + return allAlbums; } - return albumsQuery.data - ?.map((e) => ( - weightedRatio(e.name!, searchText.value), - e, - )) - .sorted((a, b) => b.$1.compareTo(a.$1)) - .where((e) => e.$1 > 50) - .map((e) => e.$2) - .toList() ?? - []; - }, [albumsQuery.data, searchText.value]); + return allAlbums + .map((e) => ( + weightedRatio(e.name!, searchText.value), + e, + )) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList(); + }, [allAlbums, searchText.value]); if (auth == null) { return const AnonymousFallback(); } + final theme = Theme.of(context); + return RefreshIndicator( onRefresh: () async { await albumsQuery.refresh(); }, - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SearchBar( + child: SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ColoredBox( + color: theme.scaffoldBackgroundColor, + child: SearchBar( onChanged: (value) => searchText.value = value, leading: const Icon(SpotubeIcons.filter), - hintText: context.l10n.filter_albums, + hintText: context.l10n.filter_artist, ), - const SizedBox(height: 20), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - firstChild: Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16.0), - child: const ShimmerPlaybuttonCard(count: 7), - ), - secondChild: Wrap( - spacing: spacing, // gap between adjacent chips - runSpacing: 20, // gap between lines - alignment: WrapAlignment.center, - children: albums - .map((album) => AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), - )) - .toList(), - ), - crossFadeState: albumsQuery.isLoading || - !albumsQuery.hasData || - searchText.value.isNotEmpty - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - ), - ], + ), + ), + ), + body: SizedBox.expand( + child: SingleChildScrollView( + padding: const EdgeInsets.all(8.0), + controller: controller, + child: Wrap( + runSpacing: 20, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (albums.isEmpty) + Container( + alignment: Alignment.topLeft, + padding: const EdgeInsets.all(16.0), + child: const ShimmerPlaybuttonCard(count: 4), + ), + for (final album in albums) + AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album(album), + ), + if (albumsQuery.hasNextPage) + Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: albumsQuery.fetchNext, + child: const ShimmerPlaybuttonCard(count: 1), + ) + ], + ), ), ), ), diff --git a/lib/components/shared/heart_button.dart b/lib/components/shared/heart_button.dart index cf790918..2b877ecf 100644 --- a/lib/components/shared/heart_button.dart +++ b/lib/components/shared/heart_button.dart @@ -1,4 +1,5 @@ import 'package:fl_query/fl_query.dart'; +import 'package:fl_query_hooks/fl_query_hooks.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -188,6 +189,7 @@ class AlbumHeartButton extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final client = useQueryClient(); final me = useQueries.user.me(ref); final albumIsSaved = useQueries.album.isSavedForMe(ref, album.id!); @@ -196,10 +198,10 @@ class AlbumHeartButton extends HookConsumerWidget { final toggleAlbumLike = useMutations.album.toggleFavorite( ref, album.id!, - refreshQueries: [ - albumIsSaved.key, - "current-user-albums", - ], + refreshQueries: [albumIsSaved.key], + onData: (_, __) async { + await client.refreshInfiniteQueryAllPages("current-user-albums"); + }, ); if (me.isLoading || !me.hasData) { diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 4879867b..fdb05673 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -532,7 +532,13 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final cached = await SkipSegment.box.get(id); if (cached != null && cached.isNotEmpty) { return List.castFrom( - (cached as List).map((json) => SkipSegment.fromJson(json)).toList(), + (cached as List) + .map( + (json) => SkipSegment.fromJson( + Map.castFrom(json), + ), + ) + .toList(), ); } diff --git a/lib/services/mutations/album.dart b/lib/services/mutations/album.dart index 920e11c2..dfc72fcc 100644 --- a/lib/services/mutations/album.dart +++ b/lib/services/mutations/album.dart @@ -9,6 +9,8 @@ class AlbumMutations { WidgetRef ref, String albumId, { List? refreshQueries, + List? refreshInfiniteQueries, + MutationOnDataFn? onData, }) { return useSpotifyMutation( "toggle-album-like/$albumId", @@ -22,6 +24,8 @@ class AlbumMutations { }, ref: ref, refreshQueries: refreshQueries, + refreshInfiniteQueries: refreshInfiniteQueries, + onData: onData, ); } } diff --git a/lib/services/queries/album.dart b/lib/services/queries/album.dart index a0c968eb..76a7937e 100644 --- a/lib/services/queries/album.dart +++ b/lib/services/queries/album.dart @@ -9,12 +9,20 @@ import 'package:spotube/provider/user_preferences_provider.dart'; class AlbumQueries { const AlbumQueries(); - Query, dynamic> ofMine(WidgetRef ref) { - return useSpotifyQuery, dynamic>( + InfiniteQuery, dynamic, int> ofMine(WidgetRef ref) { + return useSpotifyInfiniteQuery, dynamic, int>( "current-user-albums", - (spotify) { - return spotify.me.savedAlbums().all(); + (page, spotify) { + return spotify.me.savedAlbums().getPage( + 20, + page * 20, + ); }, + initialPage: 0, + nextPage: (lastPage, lastPageData) => + (lastPageData.items?.length ?? 0) < 20 || lastPageData.isLast + ? null + : lastPage + 1, ref: ref, ); } From 190df17adcf4c01cb2bcebfdec47908828b33816 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Aug 2023 20:03:49 +0600 Subject: [PATCH 04/15] fix: jump to track going to wrong track --- lib/provider/proxy_playlist/next_fetcher_mixin.dart | 9 +++++---- lib/provider/proxy_playlist/proxy_playlist_provider.dart | 3 +++ lib/services/audio_player/mk_state_player.dart | 4 ++-- lib/utils/type_conversion_utils.dart | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index 0236ec58..c3f44015 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -86,13 +86,14 @@ mixin NextFetcher on StateNotifier { } else if (track is LocalTrack) { return track.path; } else { - return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${track.name?.replaceAll( - RegExp(r'\s+', caseSensitive: false), - '-', - )}"; + return trackToUnplayableSource(track); } } + String trackToUnplayableSource(Track track) { + return "https://youtube.com/unplayable.m4a?id=${track.id}&title=${Uri.encodeComponent(track.name!)}"; + } + List mapSourcesToTracks(List sources) { return sources .map((source) { diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index fdb05673..941951af 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -336,15 +336,18 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier Future jumpTo(int index) async { final oldTrack = mapSourcesToTracks([audioPlayer.currentSource!]).firstOrNull; + state = state.copyWith(active: index); await audioPlayer.pause(); final track = await ensureSourcePlayable(audioPlayer.sources[index]); + if (track != null) { state = state.copyWith( tracks: mergeTracks([track], state.tracks), active: index, ); } + await audioPlayer.jumpTo(index); if (oldTrack != null || track != null) { diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index 84765727..c64f0095 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -159,7 +159,7 @@ class MkPlayerWithState extends Player { @override Future next() async { - if (_playlist == null || _playlist!.index + 1 >= _playlist!.medias.length) { + if (_playlist == null) { return; } @@ -190,7 +190,7 @@ class MkPlayerWithState extends Player { @override Future jump(int index) async { if (_playlist == null || index < 0 || index >= _playlist!.medias.length) { - return null; + return; } playlist = _playlist!.copyWith(index: index); diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index 5694d3fe..cf658a0e 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -67,7 +67,7 @@ abstract class TypeConversionUtils { if (onRouteChange != null) { onRouteChange("/artist/${artist.value.id}"); } else { - ServiceUtils.navigate( + ServiceUtils.push( context, "/artist/${artist.value.id}", ); From c3c09f5b76c9547a306d15cd3768dacc1622876d Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 25 Aug 2023 22:52:43 +0600 Subject: [PATCH 05/15] fix: last track of queue never plays & repeat playlist never works --- .../proxy_playlist/next_fetcher_mixin.dart | 8 ++-- .../proxy_playlist_provider.dart | 7 ++- lib/services/audio_player/audio_player.dart | 2 +- .../audio_player/audio_player_impl.dart | 10 ++++ .../audio_player/mk_state_player.dart | 47 ++++++++++--------- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/provider/proxy_playlist/next_fetcher_mixin.dart b/lib/provider/proxy_playlist/next_fetcher_mixin.dart index c3f44015..fce006b0 100644 --- a/lib/provider/proxy_playlist/next_fetcher_mixin.dart +++ b/lib/provider/proxy_playlist/next_fetcher_mixin.dart @@ -98,10 +98,10 @@ mixin NextFetcher on StateNotifier { return sources .map((source) { final track = state.tracks.firstWhereOrNull( - (track) { - final newSource = makeAppropriateSource(track); - return newSource == source; - }, + (track) => + trackToUnplayableSource(track) == source || + (track is SpotubeTrack && track.ytUri == source) || + (track is LocalTrack && track.path == source), ); return track; }) diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index 941951af..7ab9293a 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'package:catcher/catcher.dart'; import 'package:collection/collection.dart'; @@ -129,6 +130,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + final track = await ensureSourcePlayable(audioPlayer.nextSource!); if (track != null) { @@ -439,13 +441,16 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier Future next() async { if (audioPlayer.nextSource == null) return; final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; + state = state.copyWith( active: state.tracks .toList() .indexWhere((element) => element.id == oldTrack?.id), ); + await audioPlayer.pause(); final track = await ensureSourcePlayable(audioPlayer.nextSource!); + if (track != null) { state = state.copyWith( tracks: mergeTracks([track], state.tracks), @@ -613,7 +618,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier final oldCollections = state.collections; await load( state.tracks, - initialIndex: state.active ?? 0, + initialIndex: max(state.active ?? 0, 0), autoPlay: false, ); state = state.copyWith(collections: oldCollections); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 3a54f9ba..f468e87a 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -111,7 +111,7 @@ abstract class AudioPlayerInterface { // } } - Future get loopMode async { + PlaybackLoopMode get loopMode { return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode); // if (mkSupportedPlatform) { // return PlaybackLoopMode.fromPlaylistMode(_mkPlayer.loopMode); diff --git a/lib/services/audio_player/audio_player_impl.dart b/lib/services/audio_player/audio_player_impl.dart index cca8c36c..417cf4b3 100644 --- a/lib/services/audio_player/audio_player_impl.dart +++ b/lib/services/audio_player/audio_player_impl.dart @@ -156,6 +156,12 @@ class SpotubeAudioPlayer extends AudioPlayerInterface String? get nextSource { // if (mkSupportedPlatform) { + + if (loopMode == PlaybackLoopMode.all && + _mkPlayer.playlist.index == _mkPlayer.playlist.medias.length - 1) { + return sources.first; + } + return _mkPlayer.playlist.medias .elementAtOrNull(_mkPlayer.playlist.index + 1) ?.uri; @@ -169,6 +175,10 @@ class SpotubeAudioPlayer extends AudioPlayerInterface } String? get previousSource { + if (loopMode == PlaybackLoopMode.all && _mkPlayer.playlist.index == 0) { + return sources.last; + } + // if (mkSupportedPlatform) { return _mkPlayer.playlist.medias .elementAtOrNull(_mkPlayer.playlist.index - 1) diff --git a/lib/services/audio_player/mk_state_player.dart b/lib/services/audio_player/mk_state_player.dart index c64f0095..945b2ce0 100644 --- a/lib/services/audio_player/mk_state_player.dart +++ b/lib/services/audio_player/mk_state_player.dart @@ -170,6 +170,7 @@ class MkPlayerWithState extends Player { return super.open(_playlist!.medias[_playlist!.index], play: true); } else if (!isLast) { playlist = _playlist!.copyWith(index: _playlist!.index + 1); + return super.open(_playlist!.medias[_playlist!.index], play: true); } } @@ -233,30 +234,30 @@ class MkPlayerWithState extends Player { final isOldUrlPlaying = _playlist!.medias[_playlist!.index].uri == oldUrl; - for (var i = 0; i < _playlist!.medias.length - 1; i++) { - final media = _playlist!.medias[i]; - if (media.uri == oldUrl) { - if (isOldUrlPlaying) { - pause(); - } - final newMedias = _playlist!.medias.toList(); - newMedias[i] = Media(newUrl, extras: media.extras); - playlist = _playlist!.copyWith(medias: newMedias); - if (isOldUrlPlaying) { - super.open( - newMedias[i], - play: true, - ); - } - - // replace in the _tempMedias if it's not null - if (shuffled && _tempMedias != null) { - final tempIndex = _tempMedias!.indexOf(media); - _tempMedias![tempIndex] = Media(newUrl, extras: media.extras); - } - break; + // ends the loop where match is found + // tends to be a bit more efficient than forEach + _playlist!.medias.firstWhereIndexedOrNull((i, media) { + if (media.uri != oldUrl) return false; + if (isOldUrlPlaying) { + pause(); } - } + final copyMedias = [..._playlist!.medias]; + copyMedias[i] = Media(newUrl, extras: media.extras); + playlist = _playlist!.copyWith(medias: copyMedias); + if (isOldUrlPlaying) { + super.open( + copyMedias[i], + play: true, + ); + } + + // replace in the _tempMedias if it's not null + if (shuffled && _tempMedias != null) { + final tempIndex = _tempMedias!.indexOf(media); + _tempMedias![tempIndex] = Media(newUrl, extras: media.extras); + } + return true; + }); } @override From e90eceb285a84028df690c25a687ff9b5168bba8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 00:06:41 +0600 Subject: [PATCH 06/15] fix: infinite route push glitch --- lib/utils/service_utils.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index 2d44b984..c80e527d 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -257,11 +257,19 @@ abstract class ServiceUtils { } static void navigate(BuildContext context, String location, {Object? extra}) { + if (GoRouterState.of(context).matchedLocation == location) return; GoRouter.of(context).go(location, extra: extra); } static void push(BuildContext context, String location, {Object? extra}) { - GoRouter.of(context).push(location, extra: extra); + final router = GoRouter.of(context); + final routerState = GoRouterState.of(context); + final routerStack = router.routerDelegate.currentConfiguration.matches + .map((e) => e.matchedLocation); + + if (routerState.matchedLocation == location || + routerStack.contains(location)) return; + router.push(location, extra: extra); } static List sortTracks(List tracks, SortBy sortBy) { From 08b627e86ab693cab25c8eef08664562d79ae28c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 00:11:21 +0600 Subject: [PATCH 07/15] chore: bump media_kit android audio version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 92836342..902d1ef9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,7 +69,7 @@ dependencies: logger: ^1.1.0 media_kit: ^1.1.3 media_kit_native_event_loop: ^1.0.7 - media_kit_libs_android_audio: ^1.3.1 + media_kit_libs_android_audio: ^1.3.2 media_kit_libs_ios_audio: ^1.1.2 media_kit_libs_macos_audio: ^1.1.2 media_kit_libs_windows_audio: ^1.0.6 From 1a7ea0ce6aae1a7cbe195f6b2fae7d99082bb828 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 00:40:50 +0600 Subject: [PATCH 08/15] fix: sanitize song title for file name (#644) --- lib/provider/download_manager_provider.dart | 3 ++- lib/utils/primitive_utils.dart | 4 ++++ pubspec.lock | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index b5c4a0e7..66a2b30d 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -14,6 +14,7 @@ import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/services/download_manager/download_manager.dart'; import 'package:spotube/services/youtube/youtube.dart'; +import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class DownloadManagerProvider extends ChangeNotifier { @@ -130,7 +131,7 @@ class DownloadManagerProvider extends ChangeNotifier { String getTrackFileUrl(Track track) { final name = "${track.name} - ${TypeConversionUtils.artists_X_String(track.artists ?? [])}.m4a"; - return join(downloadDirectory, name); + return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); } bool isActive(Track track) { diff --git a/lib/utils/primitive_utils.dart b/lib/utils/primitive_utils.dart index 2298f14f..a0e54430 100644 --- a/lib/utils/primitive_utils.dart +++ b/lib/utils/primitive_utils.dart @@ -57,4 +57,8 @@ abstract class PrimitiveUtils { }), ); } + + static String toSafeFileName(String str) { + return str.replaceAll(RegExp(r'[^\w\s\.\-_]'), "_"); + } } diff --git a/pubspec.lock b/pubspec.lock index 0e298616..6aa7b9cb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1090,10 +1090,10 @@ packages: dependency: "direct main" description: name: media_kit_libs_android_audio - sha256: "767a93c44da73b7103a1fcbe2346f7211b7c44fa727f359410e690a156f630c5" + sha256: f16e67d4c5a85cb603290da253456bc8ea3d85d932c778e3afd11195db2dc26d url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" media_kit_libs_ios_audio: dependency: "direct main" description: From 633415dd3e702a38c5a7e7d7b3b1c2713d9c9cc9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 09:37:45 +0600 Subject: [PATCH 09/15] fix: debian bookworm dependencies --- linux/packaging/deb/make_config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml index b8c9ec04..cd12b494 100644 --- a/linux/packaging/deb/make_config.yaml +++ b/linux/packaging/deb/make_config.yaml @@ -11,8 +11,8 @@ installed_size: 24400 dependencies: - mpv - - libappindicator3-1 - - gir1.2-appindicator3-0.1 + - libappindicator3-1 | libayatana-appindicator3-1 + - gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1 - libsecret-1-0 - libnotify-bin - libjsoncpp25 From 2a80761001332bbb9ecf90bc1301677f3085e663 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 10:52:58 +0600 Subject: [PATCH 10/15] chore: untranslated message --- untranslated_messages.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/untranslated_messages.json b/untranslated_messages.json index 71307e9a..ac25b340 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -54,6 +54,11 @@ "connection_restored" ], + "pt": [ + "you_are_offline", + "connection_restored" + ], + "zh": [ "piped_api_down", "piped_down_error_instructions", From fb360035ade09c270b46a0c3b99ab1594ece07c0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 11:02:44 +0600 Subject: [PATCH 11/15] fix: window size remains same after exiting mini player (#618) --- lib/collections/routes.dart | 4 ++-- lib/components/root/bottom_player.dart | 7 ++++++- lib/pages/lyrics/mini_lyrics.dart | 7 +++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/collections/routes.dart b/lib/collections/routes.dart index 0faf432b..faa63da8 100644 --- a/lib/collections/routes.dart +++ b/lib/collections/routes.dart @@ -128,8 +128,8 @@ final router = GoRouter( GoRoute( path: "/mini-player", parentNavigatorKey: rootNavigatorKey, - pageBuilder: (context, state) => const SpotubePage( - child: MiniLyricsPage(), + pageBuilder: (context, state) => SpotubePage( + child: MiniLyricsPage(prevSize: state.extra as Size), ), ), GoRoute( diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 5a09ffa5..6d2e8319 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -92,6 +92,8 @@ class BottomPlayer extends HookConsumerWidget { tooltip: context.l10n.mini_player, icon: const Icon(SpotubeIcons.miniPlayer), onPressed: () async { + final prevSize = + await DesktopTools.window.getSize(); await DesktopTools.window.setMinimumSize( const Size(300, 300), ); @@ -106,7 +108,10 @@ class BottomPlayer extends HookConsumerWidget { await Future.delayed( const Duration(milliseconds: 100), () async { - GoRouter.of(context).go('/mini-player'); + GoRouter.of(context).go( + '/mini-player', + extra: prevSize, + ); }, ); }, diff --git a/lib/pages/lyrics/mini_lyrics.dart b/lib/pages/lyrics/mini_lyrics.dart index e99674c8..ad3a13ef 100644 --- a/lib/pages/lyrics/mini_lyrics.dart +++ b/lib/pages/lyrics/mini_lyrics.dart @@ -19,13 +19,13 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/utils/platform.dart'; class MiniLyricsPage extends HookConsumerWidget { - const MiniLyricsPage({Key? key}) : super(key: key); + final Size prevSize; + const MiniLyricsPage({Key? key, required this.prevSize}) : super(key: key); @override Widget build(BuildContext context, ref) { final theme = Theme.of(context); final update = useForceUpdate(); - final prevSize = useRef(null); final wasMaximized = useRef(false); final playlistQueue = ref.watch(ProxyPlaylistNotifier.provider); @@ -35,7 +35,6 @@ class MiniLyricsPage extends HookConsumerWidget { useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { - prevSize.value = await DesktopTools.window.getSize(); wasMaximized.value = await DesktopTools.window.isMaximized(); }); return null; @@ -213,7 +212,7 @@ class MiniLyricsPage extends HookConsumerWidget { if (wasMaximized.value) { await DesktopTools.window.maximize(); } else { - await DesktopTools.window.setSize(prevSize.value!); + await DesktopTools.window.setSize(prevSize); } await DesktopTools.window .setAlignment(Alignment.center); From 48e90a42294a6287cad65f840a7cc305988d34ff Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 12:20:14 +0600 Subject: [PATCH 12/15] fix: sorting by date crashes app (#551) --- lib/provider/download_manager_provider.dart | 2 +- lib/utils/service_utils.dart | 23 ++++++++++++++++---- lib/utils/type_conversion_utils.dart | 24 ++------------------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 66a2b30d..78c07270 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -59,7 +59,7 @@ class DownloadManagerProvider extends ChangeNotifier { album: track.album?.name, albumArtist: track.artists?.map((a) => a.name).join(", "), year: track.album?.releaseDate != null - ? int.tryParse(track.album!.releaseDate!) ?? 1969 + ? int.tryParse(track.album!.releaseDate!.split("-").first) ?? 1969 : 1969, trackNumber: track.trackNumber, discNumber: track.discNumber, diff --git a/lib/utils/service_utils.dart b/lib/utils/service_utils.dart index c80e527d..0be1dd97 100644 --- a/lib/utils/service_utils.dart +++ b/lib/utils/service_utils.dart @@ -272,6 +272,21 @@ abstract class ServiceUtils { router.push(location, extra: extra); } + static DateTime parseSpotifyAlbumDate(AlbumSimple? album) { + if (album == null || album.releaseDate == null) { + return DateTime.parse("1975-01-01"); + } + + switch (album.releaseDatePrecision ?? DatePrecision.year) { + case DatePrecision.day: + return DateTime.parse(album.releaseDate!); + case DatePrecision.month: + return DateTime.parse("${album.releaseDate}-01"); + case DatePrecision.year: + return DateTime.parse("${album.releaseDate}-01-01"); + } + } + static List sortTracks(List tracks, SortBy sortBy) { if (sortBy == SortBy.none) return tracks; return List.from(tracks) @@ -286,12 +301,12 @@ abstract class ServiceUtils { case SortBy.ascending: return a.name?.compareTo(b.name ?? "") ?? 0; case SortBy.oldest: - final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01"); - final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01"); + final aDate = parseSpotifyAlbumDate(a.album); + final bDate = parseSpotifyAlbumDate(b.album); return aDate.compareTo(bDate); case SortBy.newest: - final aDate = DateTime.parse(a.album?.releaseDate ?? "2069-01-01"); - final bDate = DateTime.parse(b.album?.releaseDate ?? "2069-01-01"); + final aDate = parseSpotifyAlbumDate(a.album); + final bDate = parseSpotifyAlbumDate(b.album); return bDate.compareTo(aDate); case SortBy.descending: return b.name?.compareTo(a.name ?? "") ?? 0; diff --git a/lib/utils/type_conversion_utils.dart b/lib/utils/type_conversion_utils.dart index cf658a0e..68a8d9a4 100644 --- a/lib/utils/type_conversion_utils.dart +++ b/lib/utils/type_conversion_utils.dart @@ -8,9 +8,6 @@ import 'package:path/path.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/models/matched_track.dart'; -import 'package:spotube/models/spotube_track.dart'; -import 'package:spotube/services/youtube/youtube.dart'; import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -122,29 +119,12 @@ abstract class TypeConversionUtils { return track; } - static SpotubeTrack localTrack_X_Track( + static Track localTrack_X_Track( File file, { Metadata? metadata, String? art, }) { - final track = SpotubeTrack( - YoutubeVideoInfo( - searchMode: SearchMode.youtube, - id: "dQw4w9WgXcQ", - title: basenameWithoutExtension(file.path), - duration: Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0), - dislikes: 0, - likes: 0, - thumbnailUrl: art ?? "", - views: 0, - channelName: metadata?.albumArtist ?? "Spotube", - channelId: metadata?.albumArtist ?? "Spotube", - publishedAt: - metadata?.year != null ? DateTime(metadata!.year!) : DateTime(2003), - ), - file.path, - [], - ); + final track = Track(); track.album = Album() ..name = metadata?.album ?? "Spotube" ..images = [if (art != null) Image()..url = art] From a14fb9ec389822e5ffa0c537e162b87cbba34e6c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 12:38:29 +0600 Subject: [PATCH 13/15] feat: jump to specific time on lyric click (#590) --- lib/pages/lyrics/synced_lyrics.dart | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 2566a7a2..9a3d2ac5 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,6 +12,7 @@ import 'package:spotube/hooks/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/use_synced_lyrics.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; @@ -114,9 +116,7 @@ class SyncedLyrics extends HookConsumerWidget { ? Container( padding: index == lyricValue.lyrics.length - 1 ? EdgeInsets.only( - bottom: - MediaQuery.of(context).size.height / - 2, + bottom: mediaQuery.size.height / 2, ) : null, ) @@ -138,11 +138,25 @@ class SyncedLyrics extends HookConsumerWidget { : FontWeight.normal, fontSize: (isActive ? 28 : 26) * (textZoomLevel.value / 100), - shadows: kElevationToShadow[9], ), - child: Text( - lyricSlice.text, - textAlign: TextAlign.center, + child: InkWell( + onTap: () async { + final duration = + await audioPlayer.duration ?? + Duration.zero; + final time = Duration( + seconds: + lyricSlice.time.inSeconds - delay, + ); + if (time > duration || time.isNegative) { + return; + } + audioPlayer.seek(time); + }, + child: Text( + lyricSlice.text, + textAlign: TextAlign.center, + ), ), ), ), From 8d4602962be20ea4bafc20db10eae1160f83ac52 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 13:52:59 +0600 Subject: [PATCH 14/15] feat: ability to toggle system title bar & custom title bar (#185) --- lib/collections/spotube_icons.dart | 1 + .../shared/page_window_title_bar.dart | 33 +++++++++---------- lib/l10n/app_en.arb | 3 +- lib/pages/settings/settings.dart | 6 ++++ lib/provider/user_preferences_provider.dart | 18 ++++++++++ untranslated_messages.json | 30 +++++++++++------ 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index a0fd3922..5503ebb3 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -93,4 +93,5 @@ abstract class SpotubeIcons { static const skip = FeatherIcons.fastForward; static const noWifi = FeatherIcons.wifiOff; static const wifi = FeatherIcons.wifi; + static const window = Icons.window_rounded; } diff --git a/lib/components/shared/page_window_title_bar.dart b/lib/components/shared/page_window_title_bar.dart index b46795c1..0d35a428 100644 --- a/lib/components/shared/page_window_title_bar.dart +++ b/lib/components/shared/page_window_title_bar.dart @@ -21,7 +21,7 @@ final closeNotification = DesktopTools.createNotification( windowManager.close(); }; -class PageWindowTitleBar extends StatefulHookWidget +class PageWindowTitleBar extends StatefulHookConsumerWidget implements PreferredSizeWidget { final Widget? leading; final bool automaticallyImplyLeading; @@ -60,23 +60,23 @@ class PageWindowTitleBar extends StatefulHookWidget Size get preferredSize => const Size.fromHeight(kToolbarHeight); @override - State createState() => _PageWindowTitleBarState(); + ConsumerState createState() => _PageWindowTitleBarState(); } -class _PageWindowTitleBarState extends State { +class _PageWindowTitleBarState extends ConsumerState { + void onDrag(details) { + final systemTitleBar = + ref.read(userPreferencesProvider.select((s) => s.systemTitleBar)); + if (kIsDesktop && !systemTitleBar) { + DesktopTools.window.startDragging(); + } + } + @override Widget build(BuildContext context) { return GestureDetector( - onHorizontalDragStart: (details) { - if (kIsDesktop) { - windowManager.startDragging(); - } - }, - onVerticalDragStart: (details) { - if (kIsDesktop) { - windowManager.startDragging(); - } - }, + onHorizontalDragStart: onDrag, + onVerticalDragStart: onDrag, child: AppBar( leading: widget.leading, automaticallyImplyLeading: widget.automaticallyImplyLeading, @@ -108,13 +108,12 @@ class WindowTitleBarButtons extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final closeBehavior = - ref.watch(userPreferencesProvider.select((s) => s.closeBehavior)); + final preferences = ref.watch(userPreferencesProvider); final isMaximized = useState(null); const type = ThemeType.auto; Future onClose() async { - if (closeBehavior == CloseBehavior.close) { + if (preferences.closeBehavior == CloseBehavior.close) { await windowManager.close(); } else { await windowManager.hide(); @@ -131,7 +130,7 @@ class WindowTitleBarButtons extends HookConsumerWidget { return null; }, []); - if (!kIsDesktop || kIsMacOS) { + if (!kIsDesktop || kIsMacOS || preferences.systemTitleBar) { return const SizedBox.shrink(); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a99757da..74894612 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -258,5 +258,6 @@ "piped_api_down": "Piped API is down", "piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change", "you_are_offline": "You are currently offline", - "connection_restored": "Your internet connection was restored" + "connection_restored": "Your internet connection was restored", + "use_system_title_bar": "Use system title bar" } \ No newline at end of file diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 033d2946..4e3fa92c 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -496,6 +496,12 @@ class SettingsPage extends HookConsumerWidget { value: preferences.showSystemTrayIcon, onChanged: preferences.setShowSystemTrayIcon, ), + SwitchListTile( + secondary: const Icon(SpotubeIcons.window), + title: Text(context.l10n.use_system_title_bar), + value: preferences.systemTitleBar, + onChanged: preferences.setSystemTitleBar, + ), ], ), if (!kIsWeb) diff --git a/lib/provider/user_preferences_provider.dart b/lib/provider/user_preferences_provider.dart index e1df5bfe..cedf6273 100644 --- a/lib/provider/user_preferences_provider.dart +++ b/lib/provider/user_preferences_provider.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; @@ -65,6 +66,8 @@ class UserPreferences extends PersistedChangeNotifier { YoutubeApiType youtubeApiType; + bool systemTitleBar; + final Ref ref; UserPreferences( @@ -85,6 +88,7 @@ class UserPreferences extends PersistedChangeNotifier { this.searchMode = SearchMode.youtube, this.skipNonMusic = true, this.youtubeApiType = YoutubeApiType.youtube, + this.systemTitleBar = false, }) : super() { if (downloadLocation.isEmpty && !kIsWeb) { _getDefaultDownloadDirectory().then( @@ -197,6 +201,15 @@ class UserPreferences extends PersistedChangeNotifier { updatePersistence(); } + void setSystemTitleBar(bool isSystemTitleBar) { + systemTitleBar = isSystemTitleBar; + DesktopTools.window.setTitleBarStyle( + systemTitleBar ? TitleBarStyle.normal : TitleBarStyle.hidden, + ); + notifyListeners(); + updatePersistence(); + } + Future _getDefaultDownloadDirectory() async { if (kIsAndroid) return "/storage/emulated/0/Download/Spotube"; @@ -257,6 +270,10 @@ class UserPreferences extends PersistedChangeNotifier { (type) => type.name == map["youtubeApiType"], orElse: () => YoutubeApiType.youtube, ); + + systemTitleBar = map["systemTitleBar"] ?? systemTitleBar; + // updates the title bar + setSystemTitleBar(systemTitleBar); } @override @@ -279,6 +296,7 @@ class UserPreferences extends PersistedChangeNotifier { "searchMode": searchMode.name, "skipNonMusic": skipNonMusic, "youtubeApiType": youtubeApiType.name, + 'systemTitleBar': systemTitleBar, }; } diff --git a/untranslated_messages.json b/untranslated_messages.json index ac25b340..07669d98 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -3,7 +3,8 @@ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "ca": [ @@ -11,58 +12,67 @@ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "de": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "es": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "fr": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "hi": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "ja": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "pl": [ "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "pt": [ "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ], "zh": [ "piped_api_down", "piped_down_error_instructions", "you_are_offline", - "connection_restored" + "connection_restored", + "use_system_title_bar" ] } From 179d536ccc10a5e63f11a63680a6e61c2d1314c8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Aug 2023 15:28:58 +0600 Subject: [PATCH 15/15] fix: lyrics page text contrast --- lib/pages/lyrics/synced_lyrics.dart | 25 ++++++++++++++++--------- lib/pages/settings/settings.dart | 12 +++++++----- pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 9a3d2ac5..dab2103d 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -1,9 +1,8 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:palette_generator/palette_generator.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Offset; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/lyrics/zoom_controls.dart'; import 'package:spotube/components/shared/shimmers/shimmer_lyrics.dart'; @@ -16,6 +15,7 @@ import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/queries/queries.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:stroke_text/stroke_text.dart'; final _delay = StateProvider((ref) => 0); @@ -130,15 +130,13 @@ class SyncedLyrics extends HookConsumerWidget { child: AnimatedDefaultTextStyle( duration: const Duration(milliseconds: 250), style: TextStyle( - color: isActive - ? Colors.white - : palette.bodyTextColor, fontWeight: isActive ? FontWeight.w500 : FontWeight.normal, fontSize: (isActive ? 28 : 26) * (textZoomLevel.value / 100), ), + textAlign: TextAlign.center, child: InkWell( onTap: () async { final duration = @@ -153,10 +151,19 @@ class SyncedLyrics extends HookConsumerWidget { } audioPlayer.seek(time); }, - child: Text( - lyricSlice.text, - textAlign: TextAlign.center, - ), + child: Builder(builder: (context) { + return StrokeText( + text: lyricSlice.text, + textStyle: + DefaultTextStyle.of(context).style, + textColor: isActive + ? Colors.white + : palette.bodyTextColor, + strokeColor: isActive + ? Colors.black + : Colors.transparent, + ); + }), ), ), ), diff --git a/lib/pages/settings/settings.dart b/lib/pages/settings/settings.dart index 4e3fa92c..d4e1e9db 100644 --- a/lib/pages/settings/settings.dart +++ b/lib/pages/settings/settings.dart @@ -336,15 +336,17 @@ class SettingsPage extends HookConsumerWidget { text: TextSpan( children: [ TextSpan( - text: context - .l10n.piped_description), + text: context + .l10n.piped_description, + style: + theme.textTheme.bodyMedium, + ), const TextSpan(text: "\n"), TextSpan( text: context.l10n.piped_warning, - style: Theme.of(context) - .textTheme - .labelMedium, + style: + theme.textTheme.labelMedium, ) ], ), diff --git a/pubspec.lock b/pubspec.lock index 56f52e9e..9c264ee2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1715,6 +1715,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + stroke_text: + dependency: "direct main" + description: + name: stroke_text + sha256: "0ec0e526c0eae7d21ce628d78eb9ae9be634259f26b0f1735f9ed540890d8cf6" + url: "https://pub.dev" + source: hosted + version: "0.0.2" supabase: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 52d2923c..a26f3676 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -103,6 +103,7 @@ dependencies: ref: a738913c8ce2c9f47515382d40827e794a334274 path: plugins/window_size youtube_explode_dart: ^2.0.1 + stroke_text: ^0.0.2 dev_dependencies: build_runner: ^2.3.2