diff --git a/lib/extension.dart b/lib/extension.dart new file mode 100644 index 0000000..2bff7e4 --- /dev/null +++ b/lib/extension.dart @@ -0,0 +1,10 @@ +export 'package:collection/collection.dart'; + +export 'extension/devices.dart'; +export 'extension/list.dart'; +export 'extension/numbers.dart'; +export 'extension/providers.dart'; +export 'extension/strings.dart'; +export 'navigation/common/material/theme.dart'; + +void importExtension() {} diff --git a/lib/extension/devices.dart b/lib/extension/devices.dart new file mode 100644 index 0000000..680400d --- /dev/null +++ b/lib/extension/devices.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; + +extension TargetPlatformExtension on TargetPlatform { + bool isMobile() => + this == TargetPlatform.android || this == TargetPlatform.iOS; + + bool isDesktop() => + this == TargetPlatform.macOS || + this == TargetPlatform.windows || + this == TargetPlatform.linux; + + bool isIos() => this == TargetPlatform.iOS; +} diff --git a/lib/extension/list.dart b/lib/extension/list.dart new file mode 100644 index 0000000..c7239dd --- /dev/null +++ b/lib/extension/list.dart @@ -0,0 +1,12 @@ +extension IterableExtension2 on Iterable { + Iterable separated(T toInsert) sync* { + var i = 0; + for (final item in this) { + if (i != 0) { + yield toInsert; + } + yield item; + i++; + } + } +} diff --git a/lib/extension/numbers.dart b/lib/extension/numbers.dart new file mode 100644 index 0000000..533be08 --- /dev/null +++ b/lib/extension/numbers.dart @@ -0,0 +1,5 @@ +extension IntParser on String { + int parseToInt() { + return int.parse(this); + } +} diff --git a/lib/extension/providers.dart b/lib/extension/providers.dart new file mode 100644 index 0000000..cb706e7 --- /dev/null +++ b/lib/extension/providers.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +extension ProvidersException on ProviderBase> { + ProviderListenable> logErrorOnDebug() { + return select((value) { + if (value is AsyncError) { + final error = value as AsyncError; + debugPrint('$this: ${error.error} ${error.stackTrace}'); + } + return value; + }); + } +} + +extension ReadAsyncValueToFuture on WidgetRef { + Future readValueOrWait(ProviderBase> provider) { + final completer = Completer(); + + final value = read(provider); + if (value.hasValue) { + completer.complete(value.value); + } else if (value.hasError) { + completer.completeError(value.error!, value.stackTrace); + } else { + ProviderSubscription? subscription; + subscription = listenManual>(provider, (previous, next) { + if (next.isLoading) { + return; + } + if (next.hasValue) { + completer.complete(next.value); + } else if (next.hasError) { + completer.completeError(next.error!, next.stackTrace); + } + subscription?.close(); + }); + } + return completer.future; + } +} + +extension AutoRemover on ProviderSubscription { + void autoRemove(Ref ref) { + ref.onDispose(close); + } +} diff --git a/lib/extension/strings.dart b/lib/extension/strings.dart new file mode 100644 index 0000000..ae33d9b --- /dev/null +++ b/lib/extension/strings.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; +import '../generated/l10n.dart'; + +extension AppStringResourceExtension on BuildContext { + S get strings { + return S.of(this); + } +} diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 0000000..7cf406c --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,71 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; +import 'messages_pt.dart' as messages_pt; +import 'messages_zh.dart' as messages_zh; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new SynchronousFuture(null), + 'pt': () => new SynchronousFuture(null), + 'zh': () => new SynchronousFuture(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + case 'pt': + return messages_pt.messages; + case 'zh': + return messages_zh.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) { + var availableLocale = Intl.verifiedLocale( + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new SynchronousFuture(false); + } + var lib = _deferredLibraries[availableLocale]; + lib == null ? new SynchronousFuture(false) : lib(); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new SynchronousFuture(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart new file mode 100644 index 0000000..6f46538 --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,214 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + static String m0(artistName, albumName, albumId, sharedUserId) => + "The ${artistName}\'s album《${albumName}》: http://music.163.com/album/${albumId}/?userid=${sharedUserId} (From @NeteaseCouldMusic)"; + + static String m1(value) => "Album count: ${value}"; + + static String m2(value) => "Created at ${value}"; + + static String m3(value) => "${value} Music"; + + static String m4(value) => "Play Count: ${value}"; + + static String m5(username, title, playlistId, userId, shareUserId) => + "The PlayList created by ${username}「${title}」: http://music.163.com/playlist/${playlistId}/${userId}/?userid=${shareUserId} (From @NeteaseCouldMusic)"; + + static String m6(value) => "Track Count: ${value}"; + + static String m7(value) => "Find ${value} music"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("About"), + "account": MessageLookupByLibrary.simpleMessage("Account"), + "addAllSongsToPlaylist": + MessageLookupByLibrary.simpleMessage("Add all songs to playlist"), + "addSongToPlaylist": + MessageLookupByLibrary.simpleMessage("Add song to playlist"), + "addToPlaylist": + MessageLookupByLibrary.simpleMessage("add to playlist"), + "addToPlaylistFailed": + MessageLookupByLibrary.simpleMessage("add to playlist failed"), + "addedToPlaylistSuccess": MessageLookupByLibrary.simpleMessage( + "Added to playlist successfully"), + "album": MessageLookupByLibrary.simpleMessage("Album"), + "albumShareContent": m0, + "alreadyBuy": MessageLookupByLibrary.simpleMessage("Payed"), + "artistAlbumCount": m1, + "artists": MessageLookupByLibrary.simpleMessage("Artists"), + "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), + "clear": MessageLookupByLibrary.simpleMessage("Clear"), + "clearPlayHistory": + MessageLookupByLibrary.simpleMessage("Clear Play History"), + "cloudMusic": MessageLookupByLibrary.simpleMessage("Could Space"), + "cloudMusicFileDropDescription": MessageLookupByLibrary.simpleMessage( + "Drop your music file to here to upload."), + "cloudMusicUsage": MessageLookupByLibrary.simpleMessage("Cloud Usage"), + "collectionLike": MessageLookupByLibrary.simpleMessage("Collections"), + "confirm": MessageLookupByLibrary.simpleMessage("Confirm"), + "copyRightOverlay": MessageLookupByLibrary.simpleMessage( + "Only used for personal study and research, commercial and illegal purposes are prohibited"), + "createdDate": m2, + "createdSongList": + MessageLookupByLibrary.simpleMessage("Created Song List"), + "currentPlaying": + MessageLookupByLibrary.simpleMessage("Current Playing"), + "dailyRecommend": + MessageLookupByLibrary.simpleMessage("Daily Recommend"), + "dailyRecommendDescription": MessageLookupByLibrary.simpleMessage( + "Daily recommend music from Netease cloud music. Refresh every day at 06:00."), + "delete": MessageLookupByLibrary.simpleMessage("delete"), + "discover": MessageLookupByLibrary.simpleMessage("Discover"), + "duration": MessageLookupByLibrary.simpleMessage("Duration"), + "errorNotLogin": + MessageLookupByLibrary.simpleMessage("Please login first."), + "errorToFetchData": + MessageLookupByLibrary.simpleMessage("error to fetch data."), + "events": MessageLookupByLibrary.simpleMessage("Events"), + "failedToDelete": MessageLookupByLibrary.simpleMessage("delete failed"), + "failedToLoad": MessageLookupByLibrary.simpleMessage("failed to load"), + "failedToPlayMusic": + MessageLookupByLibrary.simpleMessage("failed to play music"), + "favoriteSongList": + MessageLookupByLibrary.simpleMessage("Favorite Song List"), + "follow": MessageLookupByLibrary.simpleMessage("Follow"), + "follower": MessageLookupByLibrary.simpleMessage("Follower"), + "friends": MessageLookupByLibrary.simpleMessage("Friends"), + "functionDescription": + MessageLookupByLibrary.simpleMessage("Description"), + "hideCopyrightOverlay": + MessageLookupByLibrary.simpleMessage("Hide Copyright Overlay"), + "intelligenceRecommended": + MessageLookupByLibrary.simpleMessage("Intelligence Recommended"), + "keySpace": MessageLookupByLibrary.simpleMessage("Space"), + "latestPlayHistory": + MessageLookupByLibrary.simpleMessage("Play History"), + "leaderboard": MessageLookupByLibrary.simpleMessage("Leaderboard"), + "library": MessageLookupByLibrary.simpleMessage("Library"), + "likeMusic": MessageLookupByLibrary.simpleMessage("Like Music"), + "loading": MessageLookupByLibrary.simpleMessage("Loading..."), + "localMusic": MessageLookupByLibrary.simpleMessage("Local Music"), + "login": MessageLookupByLibrary.simpleMessage("Login"), + "loginViaQrCode": + MessageLookupByLibrary.simpleMessage("Login via QR code"), + "loginViaQrCodeWaitingConfirmDescription": + MessageLookupByLibrary.simpleMessage( + "Please confirm login via QR code in Netease cloud music mobile app"), + "loginViaQrCodeWaitingScanDescription": + MessageLookupByLibrary.simpleMessage( + "scan QR code by Netease cloud music mobile app"), + "loginWithPhone": + MessageLookupByLibrary.simpleMessage("login with phone"), + "logout": MessageLookupByLibrary.simpleMessage("Logout"), + "musicCountFormat": m3, + "musicName": MessageLookupByLibrary.simpleMessage("Music Name"), + "my": MessageLookupByLibrary.simpleMessage("My"), + "myDjs": MessageLookupByLibrary.simpleMessage("Dj"), + "myFavoriteMusics": + MessageLookupByLibrary.simpleMessage("My Favorite Musics"), + "myMusic": MessageLookupByLibrary.simpleMessage("My Music"), + "nextStep": MessageLookupByLibrary.simpleMessage("next step"), + "noLyric": MessageLookupByLibrary.simpleMessage("No Lyric"), + "noMusic": MessageLookupByLibrary.simpleMessage("no music"), + "noPlayHistory": + MessageLookupByLibrary.simpleMessage("No play history"), + "pause": MessageLookupByLibrary.simpleMessage("Pause"), + "personalFM": MessageLookupByLibrary.simpleMessage("Personal FM"), + "personalFmPlaying": + MessageLookupByLibrary.simpleMessage("Personal FM Playing"), + "personalProfile": + MessageLookupByLibrary.simpleMessage("Personal profile"), + "play": MessageLookupByLibrary.simpleMessage("Play"), + "playAll": MessageLookupByLibrary.simpleMessage("Play All"), + "playInNext": MessageLookupByLibrary.simpleMessage("play in next"), + "playOrPause": MessageLookupByLibrary.simpleMessage("Play/Pause"), + "playingList": MessageLookupByLibrary.simpleMessage("Playing List"), + "playlist": MessageLookupByLibrary.simpleMessage("PlayList"), + "playlistLoginDescription": MessageLookupByLibrary.simpleMessage( + "Login to discover your playlists."), + "playlistPlayCount": m4, + "playlistShareContent": m5, + "playlistTrackCount": m6, + "pleaseInputPassword": + MessageLookupByLibrary.simpleMessage("Please input password"), + "projectDescription": MessageLookupByLibrary.simpleMessage( + "OpenSource project https://github.com/boyan01/flutter-netease-music"), + "qrCodeExpired": + MessageLookupByLibrary.simpleMessage("QR code expired"), + "recommendForYou": + MessageLookupByLibrary.simpleMessage("Recommend for you"), + "recommendPlayLists": + MessageLookupByLibrary.simpleMessage("Recommend PlayLists"), + "recommendTrackIconText": MessageLookupByLibrary.simpleMessage("R"), + "remove": MessageLookupByLibrary.simpleMessage("Remove"), + "repeatModePlayIntelligence": + MessageLookupByLibrary.simpleMessage("Play Intelligence"), + "repeatModePlaySequence": + MessageLookupByLibrary.simpleMessage("Play Sequence"), + "repeatModePlayShuffle": + MessageLookupByLibrary.simpleMessage("Play Shuffle"), + "repeatModePlaySingle": + MessageLookupByLibrary.simpleMessage("Play Single"), + "search": MessageLookupByLibrary.simpleMessage("Search"), + "searchHistory": MessageLookupByLibrary.simpleMessage("Search History"), + "searchMusicResultCount": m7, + "searchPlaylistSongs": + MessageLookupByLibrary.simpleMessage("Search Songs"), + "selectRegionDiaCode": + MessageLookupByLibrary.simpleMessage("select region code"), + "selectTheArtist": + MessageLookupByLibrary.simpleMessage("Select the artist"), + "settings": MessageLookupByLibrary.simpleMessage("Settings"), + "share": MessageLookupByLibrary.simpleMessage("Share"), + "shareContentCopied": MessageLookupByLibrary.simpleMessage( + "Share content has copied to clipboard."), + "shortcuts": MessageLookupByLibrary.simpleMessage("Shortcuts"), + "showAllHotSongs": + MessageLookupByLibrary.simpleMessage("Show all hot songs >"), + "skipAccompaniment": MessageLookupByLibrary.simpleMessage( + "Skip accompaniment when play playlist."), + "skipLogin": MessageLookupByLibrary.simpleMessage("Skip login"), + "skipToNext": MessageLookupByLibrary.simpleMessage("Skip to Next"), + "skipToPrevious": + MessageLookupByLibrary.simpleMessage("Skip to Previous"), + "songs": MessageLookupByLibrary.simpleMessage("Songs"), + "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), + "sureToClearPlayingList": + MessageLookupByLibrary.simpleMessage("Sure to clear playing list?"), + "sureToRemoveMusicFromPlaylist": MessageLookupByLibrary.simpleMessage( + "Sure to remove music from playlist?"), + "theme": MessageLookupByLibrary.simpleMessage("Theme"), + "themeAuto": MessageLookupByLibrary.simpleMessage("Follow System"), + "themeDark": MessageLookupByLibrary.simpleMessage("Dark"), + "themeLight": MessageLookupByLibrary.simpleMessage("Light"), + "tipsAutoRegisterIfUserNotExist": + MessageLookupByLibrary.simpleMessage("未注册手机号登陆后将自动创建账号"), + "todo": MessageLookupByLibrary.simpleMessage("TBD"), + "topSongs": MessageLookupByLibrary.simpleMessage("Top Songs"), + "trackNoCopyright": + MessageLookupByLibrary.simpleMessage("Track No Copyright"), + "volumeDown": MessageLookupByLibrary.simpleMessage("Volume Down"), + "volumeUp": MessageLookupByLibrary.simpleMessage("Volume Up") + }; +} diff --git a/lib/generated/intl/messages_pt.dart b/lib/generated/intl/messages_pt.dart new file mode 100644 index 0000000..f5bc8fa --- /dev/null +++ b/lib/generated/intl/messages_pt.dart @@ -0,0 +1,193 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a pt locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'pt'; + + static String m0(artistName, albumName, albumId, sharedUserId) => + "The ${artistName}\'s album《${albumName}》: http://music.163.com/album/${albumId}/?userid=${sharedUserId} (From @NeteaseCouldMusic)"; + + static String m1(value) => "Contagem de album: ${value}"; + + static String m2(value) => "Data de criação ${value}"; + + static String m3(value) => "Música ${value}"; + + static String m4(value) => "Contagem de reproduções: ${value}"; + + static String m5(username, title, playlistId, userId, shareUserId) => + "Lista de reprodução criada por ${username}「${title}」: http://music.163.com/playlist/${playlistId}/${userId}/?userid=${shareUserId} (From @NeteaseCouldMusic)"; + + static String m6(value) => "Track count: ${value}"; + + static String m7(value) => "Encontrar música ${value}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("Sobre"), + "addToPlaylist": MessageLookupByLibrary.simpleMessage( + "adicionar a lista de reprodução"), + "addToPlaylistFailed": MessageLookupByLibrary.simpleMessage( + "falha ao adicionar a lista de reprodução"), + "addedToPlaylistSuccess": MessageLookupByLibrary.simpleMessage( + "Adicionado a lista de reprodução com sucesso"), + "album": MessageLookupByLibrary.simpleMessage("Album"), + "albumShareContent": m0, + "alreadyBuy": MessageLookupByLibrary.simpleMessage("Pago (comprado)"), + "artistAlbumCount": m1, + "artists": MessageLookupByLibrary.simpleMessage("Artistas"), + "clearPlayHistory": MessageLookupByLibrary.simpleMessage( + "Limpar histórico de reprodução"), + "cloudMusic": MessageLookupByLibrary.simpleMessage("Espaço da nuvem"), + "cloudMusicFileDropDescription": MessageLookupByLibrary.simpleMessage( + "Solte seu arquivo de música aqui para fazer upload."), + "cloudMusicUsage": MessageLookupByLibrary.simpleMessage("Uso da nuvem"), + "collectionLike": MessageLookupByLibrary.simpleMessage("Coleções"), + "copyRightOverlay": MessageLookupByLibrary.simpleMessage( + "Usado apenas para estudo e pesquisa pessoal, usos comerciais e ilegais são proibidos"), + "createdDate": m2, + "createdSongList": + MessageLookupByLibrary.simpleMessage("Lista de músicas criadas"), + "currentPlaying": MessageLookupByLibrary.simpleMessage("Tocanto"), + "dailyRecommend": + MessageLookupByLibrary.simpleMessage("Recomendações díarias"), + "dailyRecommendDescription": MessageLookupByLibrary.simpleMessage( + "Recomendar diariamente músicas de Netease. Atualiza todos os dias às 06:00."), + "delete": MessageLookupByLibrary.simpleMessage("apagar"), + "discover": MessageLookupByLibrary.simpleMessage("Descobrir"), + "duration": MessageLookupByLibrary.simpleMessage("Duração"), + "errorNotLogin": MessageLookupByLibrary.simpleMessage( + "Conectar uma conta primeiro."), + "errorToFetchData": + MessageLookupByLibrary.simpleMessage("erro ao buscar dados."), + "events": MessageLookupByLibrary.simpleMessage("Eventos"), + "failedToDelete": + MessageLookupByLibrary.simpleMessage("falha ao apagar"), + "failedToLoad": + MessageLookupByLibrary.simpleMessage("Falha ao carregar"), + "failedToPlayMusic": + MessageLookupByLibrary.simpleMessage("falha ao reproduzir música"), + "favoriteSongList": + MessageLookupByLibrary.simpleMessage("Lista de músicas favoritas"), + "follow": MessageLookupByLibrary.simpleMessage("Seguir"), + "follower": MessageLookupByLibrary.simpleMessage("Seguidor"), + "friends": MessageLookupByLibrary.simpleMessage("Amigos"), + "functionDescription": + MessageLookupByLibrary.simpleMessage("Descrição"), + "hideCopyrightOverlay": MessageLookupByLibrary.simpleMessage( + "Ocultar sobreposição de direitos autorais"), + "keySpace": MessageLookupByLibrary.simpleMessage("Espaço"), + "latestPlayHistory": + MessageLookupByLibrary.simpleMessage("Histórico de reproduções"), + "leaderboard": + MessageLookupByLibrary.simpleMessage("Entre os melhores"), + "library": MessageLookupByLibrary.simpleMessage("Biblioteca"), + "likeMusic": MessageLookupByLibrary.simpleMessage("Como música"), + "loading": MessageLookupByLibrary.simpleMessage("carregando..."), + "localMusic": MessageLookupByLibrary.simpleMessage("Música local"), + "login": MessageLookupByLibrary.simpleMessage("conectar um conta"), + "loginViaQrCode": + MessageLookupByLibrary.simpleMessage("Conectar via QR code"), + "loginViaQrCodeWaitingConfirmDescription": + MessageLookupByLibrary.simpleMessage( + "Confirme a conexão via QR code no aplicativo para celular Netease cloud music"), + "loginViaQrCodeWaitingScanDescription": + MessageLookupByLibrary.simpleMessage( + "Escanear QR code com o aplicativo para celular netEase cloud music"), + "loginWithPhone": + MessageLookupByLibrary.simpleMessage("conectar com celular"), + "logout": MessageLookupByLibrary.simpleMessage("Desconectar conta"), + "musicCountFormat": m3, + "musicName": MessageLookupByLibrary.simpleMessage("Nome da música"), + "my": MessageLookupByLibrary.simpleMessage("Meu"), + "myDjs": MessageLookupByLibrary.simpleMessage("Dj"), + "myMusic": MessageLookupByLibrary.simpleMessage("Minhas Músicas"), + "nextStep": MessageLookupByLibrary.simpleMessage("próximo passo"), + "noLyric": MessageLookupByLibrary.simpleMessage("Sem letras"), + "noMusic": MessageLookupByLibrary.simpleMessage("sem músicas"), + "noPlayHistory": + MessageLookupByLibrary.simpleMessage("Sem histórico de reprodução"), + "pause": MessageLookupByLibrary.simpleMessage("Pausar"), + "personalFM": MessageLookupByLibrary.simpleMessage("FM personalizada"), + "personalFmPlaying": + MessageLookupByLibrary.simpleMessage("Tocando FM personalizada"), + "personalProfile": + MessageLookupByLibrary.simpleMessage("Perfil Pessoal"), + "play": MessageLookupByLibrary.simpleMessage("Reproduzir"), + "playAll": MessageLookupByLibrary.simpleMessage("Tocar tudo"), + "playInNext": MessageLookupByLibrary.simpleMessage("Tocar na próxima"), + "playOrPause": + MessageLookupByLibrary.simpleMessage("Reproduzir/Pausar"), + "playingList": MessageLookupByLibrary.simpleMessage("Tocando lista"), + "playlist": MessageLookupByLibrary.simpleMessage("Lista de reprodução"), + "playlistLoginDescription": MessageLookupByLibrary.simpleMessage( + "conecte sua conta para descobrir suas listas de reprodução."), + "playlistPlayCount": m4, + "playlistShareContent": m5, + "playlistTrackCount": m6, + "pleaseInputPassword": + MessageLookupByLibrary.simpleMessage("Por favor digite a senha"), + "projectDescription": MessageLookupByLibrary.simpleMessage( + "Projeto de código aberto https://github.com/boyan01/flutter-netease-music"), + "qrCodeExpired": + MessageLookupByLibrary.simpleMessage("QR code expirado"), + "recommendForYou": + MessageLookupByLibrary.simpleMessage("Recomendado para você"), + "recommendPlayLists": MessageLookupByLibrary.simpleMessage( + "Listas de reprodução recomendadas"), + "search": MessageLookupByLibrary.simpleMessage("Pesquisar"), + "searchHistory": + MessageLookupByLibrary.simpleMessage("Histórico de pesquisa"), + "searchMusicResultCount": m7, + "searchPlaylistSongs": + MessageLookupByLibrary.simpleMessage("Pesquisar músicas"), + "selectRegionDiaCode": + MessageLookupByLibrary.simpleMessage("Selecionar código de região"), + "selectTheArtist": + MessageLookupByLibrary.simpleMessage("Selecionar o artista"), + "settings": MessageLookupByLibrary.simpleMessage("configurações"), + "share": MessageLookupByLibrary.simpleMessage("Compartilhe"), + "shareContentCopied": MessageLookupByLibrary.simpleMessage( + "Copiado para a área de transferência."), + "shortcuts": MessageLookupByLibrary.simpleMessage("Atalhos"), + "showAllHotSongs": MessageLookupByLibrary.simpleMessage( + "Mostrar todas as músicas quentes >"), + "skipAccompaniment": MessageLookupByLibrary.simpleMessage( + "Pule o acompanhamento ao reproduzir a lista de reprodução."), + "skipLogin": MessageLookupByLibrary.simpleMessage("Não usar conta"), + "skipToNext": + MessageLookupByLibrary.simpleMessage("Pular para seguinte"), + "skipToPrevious": + MessageLookupByLibrary.simpleMessage("Pular para anterior"), + "songs": MessageLookupByLibrary.simpleMessage("Músicas"), + "subscribe": MessageLookupByLibrary.simpleMessage("Se inscreva"), + "theme": MessageLookupByLibrary.simpleMessage("Tema"), + "themeAuto": MessageLookupByLibrary.simpleMessage("Seguir o sistema"), + "themeDark": MessageLookupByLibrary.simpleMessage("Escuro"), + "themeLight": MessageLookupByLibrary.simpleMessage("Claro"), + "tipsAutoRegisterIfUserNotExist": + MessageLookupByLibrary.simpleMessage("未注册手机号登陆后将自动创建账号"), + "todo": MessageLookupByLibrary.simpleMessage("TBD"), + "topSongs": MessageLookupByLibrary.simpleMessage("Principais músicas"), + "trackNoCopyright": MessageLookupByLibrary.simpleMessage( + "Rastrear sem direitos autorais"), + "volumeDown": MessageLookupByLibrary.simpleMessage("Diminuir volume"), + "volumeUp": MessageLookupByLibrary.simpleMessage("Almentar volume") + }; +} diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart new file mode 100644 index 0000000..2b30031 --- /dev/null +++ b/lib/generated/intl/messages_zh.dart @@ -0,0 +1,178 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a zh locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'zh'; + + static String m0(artistName, albumName, albumId, sharedUserId) => + "分享${artistName}的专辑《${albumName}》: http://music.163.com/album/${albumId}/?userid=${sharedUserId} (来自@网易云音乐)"; + + static String m1(value) => "专辑数:${value}"; + + static String m2(value) => "${value}创建"; + + static String m3(value) => "共${value}首"; + + static String m4(value) => "播放数: ${value}"; + + static String m5(username, title, playlistId, userId, shareUserId) => + "分享${username}创建的歌单「${title}」: http://music.163.com/playlist/${playlistId}/${userId}/?userid=${shareUserId} (来自@网易云音乐)"; + + static String m6(value) => "歌曲数: ${value}"; + + static String m7(value) => "找到 ${value} 首歌曲"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "about": MessageLookupByLibrary.simpleMessage("关于"), + "account": MessageLookupByLibrary.simpleMessage("账号"), + "addAllSongsToPlaylist": + MessageLookupByLibrary.simpleMessage("添加全部歌曲到播放列表"), + "addSongToPlaylist": MessageLookupByLibrary.simpleMessage("添加歌曲到播放列表"), + "addToPlaylist": MessageLookupByLibrary.simpleMessage("加入歌单"), + "addToPlaylistFailed": MessageLookupByLibrary.simpleMessage("加入歌单失败"), + "addedToPlaylistSuccess": + MessageLookupByLibrary.simpleMessage("已添加到播放列表"), + "album": MessageLookupByLibrary.simpleMessage("专辑"), + "albumShareContent": m0, + "alreadyBuy": MessageLookupByLibrary.simpleMessage("收藏和赞"), + "artistAlbumCount": m1, + "artists": MessageLookupByLibrary.simpleMessage("歌手"), + "cancel": MessageLookupByLibrary.simpleMessage("取消"), + "clear": MessageLookupByLibrary.simpleMessage("清空"), + "clearPlayHistory": MessageLookupByLibrary.simpleMessage("清空列表"), + "cloudMusic": MessageLookupByLibrary.simpleMessage("云盘"), + "cloudMusicFileDropDescription": + MessageLookupByLibrary.simpleMessage("将音乐文件拖放到这里进行上传"), + "cloudMusicUsage": MessageLookupByLibrary.simpleMessage("云盘容量"), + "collectionLike": MessageLookupByLibrary.simpleMessage("已购"), + "confirm": MessageLookupByLibrary.simpleMessage("确定"), + "copyRightOverlay": + MessageLookupByLibrary.simpleMessage("只用作个人学习研究,禁止用于商业及非法用途"), + "createdDate": m2, + "createdSongList": MessageLookupByLibrary.simpleMessage("创建的歌单"), + "currentPlaying": MessageLookupByLibrary.simpleMessage("当前播放"), + "dailyRecommend": MessageLookupByLibrary.simpleMessage("每日推荐"), + "dailyRecommendDescription": + MessageLookupByLibrary.simpleMessage("网易云音乐每日推荐歌曲,每天 6:00 更新。"), + "delete": MessageLookupByLibrary.simpleMessage("删除"), + "discover": MessageLookupByLibrary.simpleMessage("发现"), + "duration": MessageLookupByLibrary.simpleMessage("时长"), + "errorNotLogin": MessageLookupByLibrary.simpleMessage("未登录"), + "errorToFetchData": MessageLookupByLibrary.simpleMessage("获取数据失败"), + "events": MessageLookupByLibrary.simpleMessage("动态"), + "failedToDelete": MessageLookupByLibrary.simpleMessage("删除失败"), + "failedToLoad": MessageLookupByLibrary.simpleMessage("加载失败"), + "failedToPlayMusic": MessageLookupByLibrary.simpleMessage("播放音乐失败"), + "favoriteSongList": MessageLookupByLibrary.simpleMessage("收藏的歌单"), + "follow": MessageLookupByLibrary.simpleMessage("关注"), + "follower": MessageLookupByLibrary.simpleMessage("粉丝"), + "friends": MessageLookupByLibrary.simpleMessage("我的好友"), + "functionDescription": MessageLookupByLibrary.simpleMessage("功能描述"), + "hideCopyrightOverlay": MessageLookupByLibrary.simpleMessage("隐藏版权浮层"), + "intelligenceRecommended": MessageLookupByLibrary.simpleMessage("智能推荐"), + "keySpace": MessageLookupByLibrary.simpleMessage("空格"), + "latestPlayHistory": MessageLookupByLibrary.simpleMessage("最近播放"), + "leaderboard": MessageLookupByLibrary.simpleMessage("排行榜"), + "library": MessageLookupByLibrary.simpleMessage("音乐库"), + "likeMusic": MessageLookupByLibrary.simpleMessage("喜欢歌曲"), + "loading": MessageLookupByLibrary.simpleMessage("加载中..."), + "localMusic": MessageLookupByLibrary.simpleMessage("本地音乐"), + "login": MessageLookupByLibrary.simpleMessage("立即登录"), + "loginViaQrCode": MessageLookupByLibrary.simpleMessage("扫码登录"), + "loginViaQrCodeWaitingConfirmDescription": + MessageLookupByLibrary.simpleMessage("在网易云音乐手机端确认登录"), + "loginViaQrCodeWaitingScanDescription": + MessageLookupByLibrary.simpleMessage("使用网易云音乐手机端扫码登录"), + "loginWithPhone": MessageLookupByLibrary.simpleMessage("手机号登录"), + "logout": MessageLookupByLibrary.simpleMessage("退出登录"), + "musicCountFormat": m3, + "musicName": MessageLookupByLibrary.simpleMessage("音乐标题"), + "my": MessageLookupByLibrary.simpleMessage("我的"), + "myDjs": MessageLookupByLibrary.simpleMessage("我的电台"), + "myFavoriteMusics": MessageLookupByLibrary.simpleMessage("我喜欢的音乐"), + "myMusic": MessageLookupByLibrary.simpleMessage("我的音乐"), + "nextStep": MessageLookupByLibrary.simpleMessage("下一步"), + "noLyric": MessageLookupByLibrary.simpleMessage("暂无歌词"), + "noMusic": MessageLookupByLibrary.simpleMessage("暂无音乐"), + "noPlayHistory": MessageLookupByLibrary.simpleMessage("暂无播放记录"), + "pause": MessageLookupByLibrary.simpleMessage("暂停"), + "personalFM": MessageLookupByLibrary.simpleMessage("私人FM"), + "personalFmPlaying": MessageLookupByLibrary.simpleMessage("私人FM播放中"), + "personalProfile": MessageLookupByLibrary.simpleMessage("个人主页"), + "play": MessageLookupByLibrary.simpleMessage("播放"), + "playAll": MessageLookupByLibrary.simpleMessage("全部播放"), + "playInNext": MessageLookupByLibrary.simpleMessage("下一首播放"), + "playOrPause": MessageLookupByLibrary.simpleMessage("播放/暂停"), + "playingList": MessageLookupByLibrary.simpleMessage("当前播放列表"), + "playlist": MessageLookupByLibrary.simpleMessage("歌单"), + "playlistLoginDescription": + MessageLookupByLibrary.simpleMessage("登录以加载你的私人播放列表。"), + "playlistPlayCount": m4, + "playlistShareContent": m5, + "playlistTrackCount": m6, + "pleaseInputPassword": MessageLookupByLibrary.simpleMessage("请输入密码"), + "projectDescription": MessageLookupByLibrary.simpleMessage( + "开源项目 https://github.com/boyan01/flutter-netease-music"), + "qrCodeExpired": MessageLookupByLibrary.simpleMessage("二维码已过期"), + "recommendForYou": MessageLookupByLibrary.simpleMessage("为你推荐"), + "recommendPlayLists": MessageLookupByLibrary.simpleMessage("推荐歌单"), + "recommendTrackIconText": MessageLookupByLibrary.simpleMessage("荐"), + "remove": MessageLookupByLibrary.simpleMessage("移除"), + "repeatModePlayIntelligence": + MessageLookupByLibrary.simpleMessage("心动模式"), + "repeatModePlaySequence": MessageLookupByLibrary.simpleMessage("顺序播放"), + "repeatModePlayShuffle": MessageLookupByLibrary.simpleMessage("随机播放"), + "repeatModePlaySingle": MessageLookupByLibrary.simpleMessage("单曲循环"), + "search": MessageLookupByLibrary.simpleMessage("搜索"), + "searchHistory": MessageLookupByLibrary.simpleMessage("搜索历史"), + "searchMusicResultCount": m7, + "searchPlaylistSongs": MessageLookupByLibrary.simpleMessage("搜索歌单歌曲"), + "selectRegionDiaCode": MessageLookupByLibrary.simpleMessage("选择地区号码"), + "selectTheArtist": MessageLookupByLibrary.simpleMessage("请选择要查看的歌手"), + "settings": MessageLookupByLibrary.simpleMessage("设置"), + "share": MessageLookupByLibrary.simpleMessage("分享"), + "shareContentCopied": + MessageLookupByLibrary.simpleMessage("分享内容已复制到剪切板"), + "shortcuts": MessageLookupByLibrary.simpleMessage("快捷键"), + "showAllHotSongs": MessageLookupByLibrary.simpleMessage("查看所有热门歌曲 >"), + "skipAccompaniment": + MessageLookupByLibrary.simpleMessage("播放歌单时跳过包含伴奏的歌曲"), + "skipLogin": MessageLookupByLibrary.simpleMessage("跳过登录"), + "skipToNext": MessageLookupByLibrary.simpleMessage("下一首"), + "skipToPrevious": MessageLookupByLibrary.simpleMessage("上一首"), + "songs": MessageLookupByLibrary.simpleMessage("歌曲"), + "subscribe": MessageLookupByLibrary.simpleMessage("收藏"), + "sureToClearPlayingList": + MessageLookupByLibrary.simpleMessage("确定清空播放列表吗?"), + "sureToRemoveMusicFromPlaylist": + MessageLookupByLibrary.simpleMessage("确定从播放列表中移除歌曲吗?"), + "theme": MessageLookupByLibrary.simpleMessage("主题"), + "themeAuto": MessageLookupByLibrary.simpleMessage("跟随系统"), + "themeDark": MessageLookupByLibrary.simpleMessage("深色主题"), + "themeLight": MessageLookupByLibrary.simpleMessage("浅色主题"), + "tipsAutoRegisterIfUserNotExist": + MessageLookupByLibrary.simpleMessage("未注册手机号登陆后将自动创建账号"), + "todo": MessageLookupByLibrary.simpleMessage("TBD"), + "topSongs": MessageLookupByLibrary.simpleMessage("热门歌曲"), + "trackNoCopyright": MessageLookupByLibrary.simpleMessage("此音乐暂无版权"), + "volumeDown": MessageLookupByLibrary.simpleMessage("音量-"), + "volumeUp": MessageLookupByLibrary.simpleMessage("音量+") + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart new file mode 100644 index 0000000..6fe17cd --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,1272 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class S { + S(); + + static S? _current; + + static S get current { + assert(_current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = S(); + S._current = instance; + + return instance; + }); + } + + static S of(BuildContext context) { + final instance = S.maybeOf(context); + assert(instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); + return instance!; + } + + static S? maybeOf(BuildContext context) { + return Localizations.of(context, S); + } + + /// `My` + String get my { + return Intl.message( + 'My', + name: 'my', + desc: '', + args: [], + ); + } + + /// `Discover` + String get discover { + return Intl.message( + 'Discover', + name: 'discover', + desc: '', + args: [], + ); + } + + /// `Local Music` + String get localMusic { + return Intl.message( + 'Local Music', + name: 'localMusic', + desc: '', + args: [], + ); + } + + /// `Could Space` + String get cloudMusic { + return Intl.message( + 'Could Space', + name: 'cloudMusic', + desc: '', + args: [], + ); + } + + /// `Play History` + String get latestPlayHistory { + return Intl.message( + 'Play History', + name: 'latestPlayHistory', + desc: '', + args: [], + ); + } + + /// `Friends` + String get friends { + return Intl.message( + 'Friends', + name: 'friends', + desc: '', + args: [], + ); + } + + /// `Dj` + String get myDjs { + return Intl.message( + 'Dj', + name: 'myDjs', + desc: '', + args: [], + ); + } + + /// `Collections` + String get collectionLike { + return Intl.message( + 'Collections', + name: 'collectionLike', + desc: '', + args: [], + ); + } + + /// `Payed` + String get alreadyBuy { + return Intl.message( + 'Payed', + name: 'alreadyBuy', + desc: '', + args: [], + ); + } + + /// `TBD` + String get todo { + return Intl.message( + 'TBD', + name: 'todo', + desc: '', + args: [], + ); + } + + /// `Login` + String get login { + return Intl.message( + 'Login', + name: 'login', + desc: '', + args: [], + ); + } + + /// `Login to discover your playlists.` + String get playlistLoginDescription { + return Intl.message( + 'Login to discover your playlists.', + name: 'playlistLoginDescription', + desc: '', + args: [], + ); + } + + /// `Created Song List` + String get createdSongList { + return Intl.message( + 'Created Song List', + name: 'createdSongList', + desc: '', + args: [], + ); + } + + /// `Favorite Song List` + String get favoriteSongList { + return Intl.message( + 'Favorite Song List', + name: 'favoriteSongList', + desc: '', + args: [], + ); + } + + /// `The PlayList created by {username}「{title}」: http://music.163.com/playlist/{playlistId}/{userId}/?userid={shareUserId} (From @NeteaseCouldMusic)` + String playlistShareContent(Object username, Object title, Object playlistId, + Object userId, Object shareUserId) { + return Intl.message( + 'The PlayList created by $username「$title」: http://music.163.com/playlist/$playlistId/$userId/?userid=$shareUserId (From @NeteaseCouldMusic)', + name: 'playlistShareContent', + desc: '', + args: [username, title, playlistId, userId, shareUserId], + ); + } + + /// `Share content has copied to clipboard.` + String get shareContentCopied { + return Intl.message( + 'Share content has copied to clipboard.', + name: 'shareContentCopied', + desc: '', + args: [], + ); + } + + /// `The {artistName}'s album《{albumName}》: http://music.163.com/album/{albumId}/?userid={sharedUserId} (From @NeteaseCouldMusic)` + String albumShareContent(Object artistName, Object albumName, Object albumId, + Object sharedUserId) { + return Intl.message( + 'The $artistName\'s album《$albumName》: http://music.163.com/album/$albumId/?userid=$sharedUserId (From @NeteaseCouldMusic)', + name: 'albumShareContent', + desc: '', + args: [artistName, albumName, albumId, sharedUserId], + ); + } + + /// `error to fetch data.` + String get errorToFetchData { + return Intl.message( + 'error to fetch data.', + name: 'errorToFetchData', + desc: '', + args: [], + ); + } + + /// `select region code` + String get selectRegionDiaCode { + return Intl.message( + 'select region code', + name: 'selectRegionDiaCode', + desc: '', + args: [], + ); + } + + /// `next step` + String get nextStep { + return Intl.message( + 'next step', + name: 'nextStep', + desc: '', + args: [], + ); + } + + /// `未注册手机号登陆后将自动创建账号` + String get tipsAutoRegisterIfUserNotExist { + return Intl.message( + '未注册手机号登陆后将自动创建账号', + name: 'tipsAutoRegisterIfUserNotExist', + desc: '', + args: [], + ); + } + + /// `login with phone` + String get loginWithPhone { + return Intl.message( + 'login with phone', + name: 'loginWithPhone', + desc: '', + args: [], + ); + } + + /// `delete` + String get delete { + return Intl.message( + 'delete', + name: 'delete', + desc: '', + args: [], + ); + } + + /// `delete failed` + String get failedToDelete { + return Intl.message( + 'delete failed', + name: 'failedToDelete', + desc: '', + args: [], + ); + } + + /// `add to playlist` + String get addToPlaylist { + return Intl.message( + 'add to playlist', + name: 'addToPlaylist', + desc: '', + args: [], + ); + } + + /// `add to playlist failed` + String get addToPlaylistFailed { + return Intl.message( + 'add to playlist failed', + name: 'addToPlaylistFailed', + desc: '', + args: [], + ); + } + + /// `play in next` + String get playInNext { + return Intl.message( + 'play in next', + name: 'playInNext', + desc: '', + args: [], + ); + } + + /// `Skip login` + String get skipLogin { + return Intl.message( + 'Skip login', + name: 'skipLogin', + desc: '', + args: [], + ); + } + + /// `Only used for personal study and research, commercial and illegal purposes are prohibited` + String get copyRightOverlay { + return Intl.message( + 'Only used for personal study and research, commercial and illegal purposes are prohibited', + name: 'copyRightOverlay', + desc: '', + args: [], + ); + } + + /// `OpenSource project https://github.com/boyan01/flutter-netease-music` + String get projectDescription { + return Intl.message( + 'OpenSource project https://github.com/boyan01/flutter-netease-music', + name: 'projectDescription', + desc: '', + args: [], + ); + } + + /// `Search` + String get search { + return Intl.message( + 'Search', + name: 'search', + desc: '', + args: [], + ); + } + + /// `My Music` + String get myMusic { + return Intl.message( + 'My Music', + name: 'myMusic', + desc: '', + args: [], + ); + } + + /// `Personal FM` + String get personalFM { + return Intl.message( + 'Personal FM', + name: 'personalFM', + desc: '', + args: [], + ); + } + + /// `failed to play music` + String get failedToPlayMusic { + return Intl.message( + 'failed to play music', + name: 'failedToPlayMusic', + desc: '', + args: [], + ); + } + + /// `no music` + String get noMusic { + return Intl.message( + 'no music', + name: 'noMusic', + desc: '', + args: [], + ); + } + + /// `PlayList` + String get playlist { + return Intl.message( + 'PlayList', + name: 'playlist', + desc: '', + args: [], + ); + } + + /// `failed to load` + String get failedToLoad { + return Intl.message( + 'failed to load', + name: 'failedToLoad', + desc: '', + args: [], + ); + } + + /// `Library` + String get library { + return Intl.message( + 'Library', + name: 'library', + desc: '', + args: [], + ); + } + + /// `Recommend PlayLists` + String get recommendPlayLists { + return Intl.message( + 'Recommend PlayLists', + name: 'recommendPlayLists', + desc: '', + args: [], + ); + } + + /// `Please login first.` + String get errorNotLogin { + return Intl.message( + 'Please login first.', + name: 'errorNotLogin', + desc: '', + args: [], + ); + } + + /// `Track Count: {value}` + String playlistTrackCount(Object value) { + return Intl.message( + 'Track Count: $value', + name: 'playlistTrackCount', + desc: '', + args: [value], + ); + } + + /// `Play Count: {value}` + String playlistPlayCount(Object value) { + return Intl.message( + 'Play Count: $value', + name: 'playlistPlayCount', + desc: '', + args: [value], + ); + } + + /// `Music Name` + String get musicName { + return Intl.message( + 'Music Name', + name: 'musicName', + desc: '', + args: [], + ); + } + + /// `Artists` + String get artists { + return Intl.message( + 'Artists', + name: 'artists', + desc: '', + args: [], + ); + } + + /// `Album` + String get album { + return Intl.message( + 'Album', + name: 'album', + desc: '', + args: [], + ); + } + + /// `Duration` + String get duration { + return Intl.message( + 'Duration', + name: 'duration', + desc: '', + args: [], + ); + } + + /// `Theme` + String get theme { + return Intl.message( + 'Theme', + name: 'theme', + desc: '', + args: [], + ); + } + + /// `Dark` + String get themeDark { + return Intl.message( + 'Dark', + name: 'themeDark', + desc: '', + args: [], + ); + } + + /// `Light` + String get themeLight { + return Intl.message( + 'Light', + name: 'themeLight', + desc: '', + args: [], + ); + } + + /// `Follow System` + String get themeAuto { + return Intl.message( + 'Follow System', + name: 'themeAuto', + desc: '', + args: [], + ); + } + + /// `Settings` + String get settings { + return Intl.message( + 'Settings', + name: 'settings', + desc: '', + args: [], + ); + } + + /// `About` + String get about { + return Intl.message( + 'About', + name: 'about', + desc: '', + args: [], + ); + } + + /// `Hide Copyright Overlay` + String get hideCopyrightOverlay { + return Intl.message( + 'Hide Copyright Overlay', + name: 'hideCopyrightOverlay', + desc: '', + args: [], + ); + } + + /// `Track No Copyright` + String get trackNoCopyright { + return Intl.message( + 'Track No Copyright', + name: 'trackNoCopyright', + desc: '', + args: [], + ); + } + + /// `No Lyric` + String get noLyric { + return Intl.message( + 'No Lyric', + name: 'noLyric', + desc: '', + args: [], + ); + } + + /// `Shortcuts` + String get shortcuts { + return Intl.message( + 'Shortcuts', + name: 'shortcuts', + desc: '', + args: [], + ); + } + + /// `Play/Pause` + String get playOrPause { + return Intl.message( + 'Play/Pause', + name: 'playOrPause', + desc: '', + args: [], + ); + } + + /// `Skip to Next` + String get skipToNext { + return Intl.message( + 'Skip to Next', + name: 'skipToNext', + desc: '', + args: [], + ); + } + + /// `Skip to Previous` + String get skipToPrevious { + return Intl.message( + 'Skip to Previous', + name: 'skipToPrevious', + desc: '', + args: [], + ); + } + + /// `Volume Up` + String get volumeUp { + return Intl.message( + 'Volume Up', + name: 'volumeUp', + desc: '', + args: [], + ); + } + + /// `Volume Down` + String get volumeDown { + return Intl.message( + 'Volume Down', + name: 'volumeDown', + desc: '', + args: [], + ); + } + + /// `Like Music` + String get likeMusic { + return Intl.message( + 'Like Music', + name: 'likeMusic', + desc: '', + args: [], + ); + } + + /// `Description` + String get functionDescription { + return Intl.message( + 'Description', + name: 'functionDescription', + desc: '', + args: [], + ); + } + + /// `Space` + String get keySpace { + return Intl.message( + 'Space', + name: 'keySpace', + desc: '', + args: [], + ); + } + + /// `Play` + String get play { + return Intl.message( + 'Play', + name: 'play', + desc: '', + args: [], + ); + } + + /// `Pause` + String get pause { + return Intl.message( + 'Pause', + name: 'pause', + desc: '', + args: [], + ); + } + + /// `Playing List` + String get playingList { + return Intl.message( + 'Playing List', + name: 'playingList', + desc: '', + args: [], + ); + } + + /// `Personal FM Playing` + String get personalFmPlaying { + return Intl.message( + 'Personal FM Playing', + name: 'personalFmPlaying', + desc: '', + args: [], + ); + } + + /// `Play All` + String get playAll { + return Intl.message( + 'Play All', + name: 'playAll', + desc: '', + args: [], + ); + } + + /// `{value} Music` + String musicCountFormat(Object value) { + return Intl.message( + '$value Music', + name: 'musicCountFormat', + desc: '', + args: [value], + ); + } + + /// `Select the artist` + String get selectTheArtist { + return Intl.message( + 'Select the artist', + name: 'selectTheArtist', + desc: '', + args: [], + ); + } + + /// `Created at {value}` + String createdDate(Object value) { + return Intl.message( + 'Created at $value', + name: 'createdDate', + desc: '', + args: [value], + ); + } + + /// `Subscribe` + String get subscribe { + return Intl.message( + 'Subscribe', + name: 'subscribe', + desc: '', + args: [], + ); + } + + /// `Share` + String get share { + return Intl.message( + 'Share', + name: 'share', + desc: '', + args: [], + ); + } + + /// `Search Songs` + String get searchPlaylistSongs { + return Intl.message( + 'Search Songs', + name: 'searchPlaylistSongs', + desc: '', + args: [], + ); + } + + /// `Skip accompaniment when play playlist.` + String get skipAccompaniment { + return Intl.message( + 'Skip accompaniment when play playlist.', + name: 'skipAccompaniment', + desc: '', + args: [], + ); + } + + /// `Daily Recommend` + String get dailyRecommend { + return Intl.message( + 'Daily Recommend', + name: 'dailyRecommend', + desc: '', + args: [], + ); + } + + /// `Daily recommend music from Netease cloud music. Refresh every day at 06:00.` + String get dailyRecommendDescription { + return Intl.message( + 'Daily recommend music from Netease cloud music. Refresh every day at 06:00.', + name: 'dailyRecommendDescription', + desc: '', + args: [], + ); + } + + /// `Current Playing` + String get currentPlaying { + return Intl.message( + 'Current Playing', + name: 'currentPlaying', + desc: '', + args: [], + ); + } + + /// `Find {value} music` + String searchMusicResultCount(Object value) { + return Intl.message( + 'Find $value music', + name: 'searchMusicResultCount', + desc: '', + args: [value], + ); + } + + /// `Songs` + String get songs { + return Intl.message( + 'Songs', + name: 'songs', + desc: '', + args: [], + ); + } + + /// `Cloud Usage` + String get cloudMusicUsage { + return Intl.message( + 'Cloud Usage', + name: 'cloudMusicUsage', + desc: '', + args: [], + ); + } + + /// `Drop your music file to here to upload.` + String get cloudMusicFileDropDescription { + return Intl.message( + 'Drop your music file to here to upload.', + name: 'cloudMusicFileDropDescription', + desc: '', + args: [], + ); + } + + /// `Album count: {value}` + String artistAlbumCount(Object value) { + return Intl.message( + 'Album count: $value', + name: 'artistAlbumCount', + desc: '', + args: [value], + ); + } + + /// `Personal profile` + String get personalProfile { + return Intl.message( + 'Personal profile', + name: 'personalProfile', + desc: '', + args: [], + ); + } + + /// `Top Songs` + String get topSongs { + return Intl.message( + 'Top Songs', + name: 'topSongs', + desc: '', + args: [], + ); + } + + /// `Show all hot songs >` + String get showAllHotSongs { + return Intl.message( + 'Show all hot songs >', + name: 'showAllHotSongs', + desc: '', + args: [], + ); + } + + /// `Please input password` + String get pleaseInputPassword { + return Intl.message( + 'Please input password', + name: 'pleaseInputPassword', + desc: '', + args: [], + ); + } + + /// `Loading...` + String get loading { + return Intl.message( + 'Loading...', + name: 'loading', + desc: '', + args: [], + ); + } + + /// `Added to playlist successfully` + String get addedToPlaylistSuccess { + return Intl.message( + 'Added to playlist successfully', + name: 'addedToPlaylistSuccess', + desc: '', + args: [], + ); + } + + /// `Clear Play History` + String get clearPlayHistory { + return Intl.message( + 'Clear Play History', + name: 'clearPlayHistory', + desc: '', + args: [], + ); + } + + /// `No play history` + String get noPlayHistory { + return Intl.message( + 'No play history', + name: 'noPlayHistory', + desc: '', + args: [], + ); + } + + /// `Login via QR code` + String get loginViaQrCode { + return Intl.message( + 'Login via QR code', + name: 'loginViaQrCode', + desc: '', + args: [], + ); + } + + /// `scan QR code by Netease cloud music mobile app` + String get loginViaQrCodeWaitingScanDescription { + return Intl.message( + 'scan QR code by Netease cloud music mobile app', + name: 'loginViaQrCodeWaitingScanDescription', + desc: '', + args: [], + ); + } + + /// `Please confirm login via QR code in Netease cloud music mobile app` + String get loginViaQrCodeWaitingConfirmDescription { + return Intl.message( + 'Please confirm login via QR code in Netease cloud music mobile app', + name: 'loginViaQrCodeWaitingConfirmDescription', + desc: '', + args: [], + ); + } + + /// `QR code expired` + String get qrCodeExpired { + return Intl.message( + 'QR code expired', + name: 'qrCodeExpired', + desc: '', + args: [], + ); + } + + /// `Recommend for you` + String get recommendForYou { + return Intl.message( + 'Recommend for you', + name: 'recommendForYou', + desc: '', + args: [], + ); + } + + /// `Logout` + String get logout { + return Intl.message( + 'Logout', + name: 'logout', + desc: '', + args: [], + ); + } + + /// `Events` + String get events { + return Intl.message( + 'Events', + name: 'events', + desc: '', + args: [], + ); + } + + /// `Follower` + String get follower { + return Intl.message( + 'Follower', + name: 'follower', + desc: '', + args: [], + ); + } + + /// `Follow` + String get follow { + return Intl.message( + 'Follow', + name: 'follow', + desc: '', + args: [], + ); + } + + /// `Search History` + String get searchHistory { + return Intl.message( + 'Search History', + name: 'searchHistory', + desc: '', + args: [], + ); + } + + /// `Leaderboard` + String get leaderboard { + return Intl.message( + 'Leaderboard', + name: 'leaderboard', + desc: '', + args: [], + ); + } + + /// `Account` + String get account { + return Intl.message( + 'Account', + name: 'account', + desc: '', + args: [], + ); + } + + /// `My Favorite Musics` + String get myFavoriteMusics { + return Intl.message( + 'My Favorite Musics', + name: 'myFavoriteMusics', + desc: '', + args: [], + ); + } + + /// `R` + String get recommendTrackIconText { + return Intl.message( + 'R', + name: 'recommendTrackIconText', + desc: '', + args: [], + ); + } + + /// `Intelligence Recommended` + String get intelligenceRecommended { + return Intl.message( + 'Intelligence Recommended', + name: 'intelligenceRecommended', + desc: '', + args: [], + ); + } + + /// `Play Single` + String get repeatModePlaySingle { + return Intl.message( + 'Play Single', + name: 'repeatModePlaySingle', + desc: '', + args: [], + ); + } + + /// `Play Shuffle` + String get repeatModePlayShuffle { + return Intl.message( + 'Play Shuffle', + name: 'repeatModePlayShuffle', + desc: '', + args: [], + ); + } + + /// `Play Sequence` + String get repeatModePlaySequence { + return Intl.message( + 'Play Sequence', + name: 'repeatModePlaySequence', + desc: '', + args: [], + ); + } + + /// `Play Intelligence` + String get repeatModePlayIntelligence { + return Intl.message( + 'Play Intelligence', + name: 'repeatModePlayIntelligence', + desc: '', + args: [], + ); + } + + /// `Sure to clear playing list?` + String get sureToClearPlayingList { + return Intl.message( + 'Sure to clear playing list?', + name: 'sureToClearPlayingList', + desc: '', + args: [], + ); + } + + /// `Cancel` + String get cancel { + return Intl.message( + 'Cancel', + name: 'cancel', + desc: '', + args: [], + ); + } + + /// `Clear` + String get clear { + return Intl.message( + 'Clear', + name: 'clear', + desc: '', + args: [], + ); + } + + /// `Add song to playlist` + String get addSongToPlaylist { + return Intl.message( + 'Add song to playlist', + name: 'addSongToPlaylist', + desc: '', + args: [], + ); + } + + /// `Add all songs to playlist` + String get addAllSongsToPlaylist { + return Intl.message( + 'Add all songs to playlist', + name: 'addAllSongsToPlaylist', + desc: '', + args: [], + ); + } + + /// `Confirm` + String get confirm { + return Intl.message( + 'Confirm', + name: 'confirm', + desc: '', + args: [], + ); + } + + /// `Sure to remove music from playlist?` + String get sureToRemoveMusicFromPlaylist { + return Intl.message( + 'Sure to remove music from playlist?', + name: 'sureToRemoveMusicFromPlaylist', + desc: '', + args: [], + ); + } + + /// `Remove` + String get remove { + return Intl.message( + 'Remove', + name: 'remove', + desc: '', + args: [], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + Locale.fromSubtags(languageCode: 'pt'), + Locale.fromSubtags(languageCode: 'zh'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/lib/main-new.dart b/lib/main-new.dart new file mode 100644 index 0000000..4a7f566 --- /dev/null +++ b/lib/main-new.dart @@ -0,0 +1,37 @@ + +import 'package:EOEFANS/navigation/app.dart'; +import 'package:EOEFANS/navigation/common/page_splash.dart'; +import 'package:EOEFANS/providers/preference_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:overlay_support/overlay_support.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +Future main() async { + final preferences = await SharedPreferences.getInstance(); + runApp( + ProviderScope( + overrides: [ + sharedPreferenceProvider.overrideWithValue(preferences), + ], + child: PageSplash( + futures: const [], + builder: (BuildContext context, List data) { + return const MyApp(); + }, + ), + ), + ); +} + + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const OverlaySupport( + child: FansApp(), + ); + } +} diff --git a/lib/media/tracks/track_list.dart b/lib/media/tracks/track_list.dart new file mode 100644 index 0000000..4a1dd24 --- /dev/null +++ b/lib/media/tracks/track_list.dart @@ -0,0 +1,84 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../repository/data/track.dart'; + +part 'track_list.g.dart'; + +const kFmTrackListId = '_fm_playlist'; + +@JsonSerializable(constructor: '_private') +class TrackList with EquatableMixin { + const TrackList._private({ + required this.id, + required this.tracks, + required this.isFM, + required this.isUserFavoriteList, + required this.rawPlaylistId, + }); + + const TrackList.empty() + : id = '', + tracks = const [], + isFM = false, + isUserFavoriteList = false, + rawPlaylistId = null; + + const TrackList.fm({required this.tracks}) + : isFM = true, + id = kFmTrackListId, + isUserFavoriteList = false, + rawPlaylistId = null; + + const TrackList.playlist({ + required this.id, + required this.tracks, + required this.rawPlaylistId, + this.isUserFavoriteList = false, + }) : assert( + id != kFmTrackListId, + 'Cannot create a playlist with id $kFmTrackListId', + ), + isFM = false; + + factory TrackList.fromJson(Map json) => + _$TrackListFromJson(json); + + final String id; + final List tracks; + + final bool isFM; + final bool isUserFavoriteList; + + // netease playlist id + final int? rawPlaylistId; + + Map toJson() => _$TrackListToJson(this); + + bool get isEmpty => id.isEmpty || tracks.isEmpty; + + TrackList copyWith({ + String? id, + List? tracks, + bool? isFM, + bool? isUserFavoriteList, + int? rawPlaylistId, + }) { + return TrackList._private( + id: id ?? this.id, + tracks: tracks ?? this.tracks, + isFM: isFM ?? this.isFM, + isUserFavoriteList: isUserFavoriteList ?? this.isUserFavoriteList, + rawPlaylistId: rawPlaylistId ?? this.rawPlaylistId, + ); + } + + @override + List get props => [ + id, + tracks, + isFM, + isUserFavoriteList, + rawPlaylistId, + ]; +} diff --git a/lib/media/tracks/track_list.g.dart b/lib/media/tracks/track_list.g.dart new file mode 100644 index 0000000..16a1697 --- /dev/null +++ b/lib/media/tracks/track_list.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'track_list.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TrackList _$TrackListFromJson(Map json) => TrackList._private( + id: json['id'] as String, + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(Map.from(e as Map))) + .toList(), + isFM: json['isFM'] as bool, + isUserFavoriteList: json['isUserFavoriteList'] as bool, + rawPlaylistId: json['rawPlaylistId'] as int?, + ); + +Map _$TrackListToJson(TrackList instance) => { + 'id': instance.id, + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), + 'isFM': instance.isFM, + 'isUserFavoriteList': instance.isUserFavoriteList, + 'rawPlaylistId': instance.rawPlaylistId, + }; diff --git a/lib/media/tracks/tracks_player.dart b/lib/media/tracks/tracks_player.dart new file mode 100644 index 0000000..e693d85 --- /dev/null +++ b/lib/media/tracks/tracks_player.dart @@ -0,0 +1,136 @@ +import 'dart:io'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../model/persistence_player_state.dart'; +import '../../repository/data/track.dart'; +import 'track_list.dart'; +// import 'tracks_player_impl_lychee.dart'; +import 'tracks_player_impl_mobile.dart'; + +enum RepeatMode { + shuffle, + single, + sequence, + heart, +} + +class TracksPlayerState with EquatableMixin { + const TracksPlayerState({ + required this.isBuffering, + required this.isPlaying, + required this.playingTrack, + required this.playingList, + required this.duration, + required this.volume, + required this.repeatMode, + }); + + final bool isBuffering; + final bool isPlaying; + final Track? playingTrack; + final TrackList playingList; + final Duration? duration; + final double volume; + final RepeatMode repeatMode; + + @override + List get props => [ + isPlaying, + isBuffering, + playingTrack, + playingList, + duration, + volume, + repeatMode, + ]; +} + +abstract class TracksPlayer extends StateNotifier { + TracksPlayer() + : super( + const TracksPlayerState( + isPlaying: false, + isBuffering: false, + playingTrack: null, + playingList: TrackList.empty(), + duration: null, + volume: 0, + repeatMode: RepeatMode.sequence, + ), + ); + + factory TracksPlayer.platform() { + // if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) { + // return TracksPlayerImplLychee(); + // } + return TracksPlayerImplMobile(); + } + + Future play(); + + Future pause(); + + Future stop(); + + Future seekTo(Duration position); + + Future setVolume(double volume); + + Future setPlaybackSpeed(double speed); + + Future skipToNext(); + + Future skipToPrevious(); + + void setRepeatMode(RepeatMode repeatMode); + + Future playFromMediaId(int trackId, {bool play = true}); + + void setTrackList(TrackList trackList); + + Future getNextTrack(); + + Future getPreviousTrack(); + + Future insertToNext(Track track); + + void restoreFromPersistence(PersistencePlayerState state); + + Track? get current; + + TrackList get trackList; + + RepeatMode get repeatMode; + + bool get isPlaying; + + bool get isBuffering; + + Duration? get position; + + Duration? get duration; + + Duration? get bufferedPosition; + + double get volume; + + double get playbackSpeed; + + bool get canAdjustVolume => true; + + @protected + void notifyPlayStateChanged() { + state = TracksPlayerState( + isPlaying: isPlaying, + isBuffering: isBuffering, + playingTrack: current, + playingList: trackList, + duration: duration, + volume: volume, + repeatMode: repeatMode, + ); + } +} diff --git a/lib/media/tracks/tracks_player_impl_lychee.dart b/lib/media/tracks/tracks_player_impl_lychee.dart new file mode 100644 index 0000000..c0a11d1 --- /dev/null +++ b/lib/media/tracks/tracks_player_impl_lychee.dart @@ -0,0 +1,286 @@ +// import 'dart:async'; +// +// import 'package:flutter/foundation.dart'; +// import 'package:lychee_player/lychee_player.dart'; +// import 'package:mixin_logger/mixin_logger.dart'; +// +// import '../../extension.dart'; +// import '../../model/persistence_player_state.dart'; +// import '../../repository.dart'; +// import '../../utils/media_cache/media_cache.dart'; +// import 'track_list.dart'; +// import 'tracks_player.dart'; +// +// extension _SecondsExt on double { +// Duration toDuration() { +// return Duration(milliseconds: (this * 1000).toInt()); +// } +// } +// +// class TracksPlayerImplLychee extends TracksPlayer { +// TracksPlayerImplLychee(); +// +// var _trackList = const TrackList.empty(); +// var _shuffleTrackIds = const []; +// +// Track? _current; +// +// LycheeAudioPlayer? _player; +// +// double _volume = 1; +// +// RepeatMode _repeatMode = RepeatMode.sequence; +// +// @override +// Duration? get bufferedPosition => null; +// +// @override +// Track? get current => _current; +// +// @override +// Duration? get duration => _player?.duration().toDuration(); +// +// @override +// Future getNextTrack() async { +// final shuffle = _repeatMode == RepeatMode.shuffle; +// if (_trackList.tracks.isEmpty) { +// assert(false, 'track list is empty'); +// return null; +// } +// if (!shuffle) { +// final index = _trackList.tracks.cast().indexOf(current) + 1; +// if (index < _trackList.tracks.length) { +// return _trackList.tracks[index]; +// } +// return _trackList.tracks.firstOrNull; +// } else { +// assert(_shuffleTrackIds.isNotEmpty, 'shuffle track ids is empty'); +// if (_shuffleTrackIds.isEmpty) { +// _generateShuffleList(); +// } +// final int index; +// if (current == null) { +// index = 0; +// } else { +// index = _shuffleTrackIds.indexOf(current!.id) + 1; +// } +// final int trackId; +// if (index < _shuffleTrackIds.length) { +// trackId = _shuffleTrackIds[index]; +// } else { +// trackId = _shuffleTrackIds.first; +// } +// return _trackList.tracks.firstWhereOrNull((e) => e.id == trackId); +// } +// } +// +// @override +// Future getPreviousTrack() async { +// final shuffle = _repeatMode == RepeatMode.shuffle; +// if (_trackList.tracks.isEmpty) { +// assert(false, 'track list is empty'); +// return null; +// } +// if (!shuffle) { +// final index = _trackList.tracks.cast().indexOf(current) - 1; +// if (index >= 0) { +// return _trackList.tracks[index]; +// } +// return _trackList.tracks.lastOrNull; +// } else { +// assert(_shuffleTrackIds.isNotEmpty, 'shuffle track ids is empty'); +// if (_shuffleTrackIds.isEmpty) { +// _generateShuffleList(); +// } +// final int index; +// if (current == null) { +// index = 0; +// } else { +// index = _shuffleTrackIds.indexOf(current!.id) - 1; +// } +// final int trackId; +// if (index >= 0) { +// trackId = _shuffleTrackIds[index]; +// } else { +// trackId = _shuffleTrackIds.last; +// } +// return _trackList.tracks.firstWhereOrNull((e) => e.id == trackId); +// } +// } +// +// @override +// Future insertToNext(Track track) async { +// final index = _trackList.tracks.cast().indexOf(current); +// if (index == -1) { +// return; +// } +// final nextIndex = index + 1; +// if (nextIndex >= _trackList.tracks.length) { +// _trackList.tracks.add(track); +// } else { +// final next = _trackList.tracks[nextIndex]; +// if (next != track) { +// _trackList.tracks.insert(nextIndex, track); +// } +// } +// notifyPlayStateChanged(); +// } +// +// @override +// bool get isBuffering => _player?.state.value == PlayerState.buffering; +// +// @override +// bool get isPlaying => +// _player?.state.value == PlayerState.ready && +// _player?.playWhenReady == true; +// +// @override +// Future pause() async { +// _player?.playWhenReady = false; +// } +// +// @override +// Future play() async { +// _player?.playWhenReady = true; +// } +// +// @override +// Future playFromMediaId(int trackId, {bool play = true}) async { +// await stop(); +// final item = _trackList.tracks.firstWhereOrNull((t) => t.id == trackId); +// if (item != null) { +// _playTrack(item, playWhenReady: play); +// } +// } +// +// @override +// double get playbackSpeed => 1; +// +// @override +// Duration? get position => _player?.currentTime().toDuration(); +// +// @override +// RepeatMode get repeatMode => _repeatMode; +// +// @override +// Future seekTo(Duration position) async { +// _player?.seek(position.inMilliseconds / 1000); +// } +// +// @override +// Future setPlaybackSpeed(double speed) async { +// // TODO implement setPlaybackSpeed +// } +// +// @override +// Future setRepeatMode(RepeatMode repeatMode) async { +// _repeatMode = repeatMode; +// notifyPlayStateChanged(); +// } +// +// void _generateShuffleList() { +// _shuffleTrackIds = trackList.tracks.map((e) => e.id).toList(); +// _shuffleTrackIds.shuffle(); +// } +// +// @override +// void setTrackList(TrackList trackList) { +// final needStop = trackList.id != _trackList.id; +// if (needStop) { +// stop(); +// _current = null; +// } +// _trackList = trackList; +// _generateShuffleList(); +// notifyPlayStateChanged(); +// } +// +// @override +// Future setVolume(double volume) async { +// _player?.volume = volume; +// _volume = volume; +// notifyPlayStateChanged(); +// } +// +// @override +// Future skipToNext() async { +// final next = await getNextTrack(); +// if (next != null) { +// _playTrack(next); +// } +// } +// +// @override +// Future skipToPrevious() async { +// final previous = await getPreviousTrack(); +// if (previous != null) { +// _playTrack(previous); +// } +// } +// +// @override +// Future stop() async { +// _player?.playWhenReady = false; +// _player?.dispose(); +// _player = null; +// _current = null; +// notifyPlayStateChanged(); +// } +// +// @override +// TrackList get trackList => _trackList; +// +// @override +// double get volume => _volume; +// +// void _playTrack( +// Track track, { +// bool playWhenReady = true, +// }) { +// scheduleMicrotask(() async { +// // final urlResult = await neteaseRepository!.getPlayUrl(track.id); +// // if (urlResult.isError) { +// // debugPrint('Failed to get play urlResult: ${urlResult.asError!.error}'); +// // return; +// // } +// // final url = +// // await generateTrackProxyUrl(track.id, urlResult.asValue!.value); +// final url = "aaa"; +// d('Play url: $url'); +// if (_current != track) { +// // skip play. since the track is changed. +// return; +// } +// _player?.dispose(); +// _player = LycheeAudioPlayer(url) +// ..playWhenReady = playWhenReady +// ..onPlayWhenReadyChanged.addListener(notifyPlayStateChanged) +// ..state.addListener(() { +// if (_player?.state.value == PlayerState.end) { +// final isSingle = _repeatMode == RepeatMode.single; +// if (isSingle) { +// _playTrack(track); +// } else { +// skipToNext(); +// } +// } +// notifyPlayStateChanged(); +// }) +// ..volume = _volume; +// }); +// _current = track; +// notifyPlayStateChanged(); +// } +// +// @override +// void restoreFromPersistence(PersistencePlayerState state) { +// _trackList = state.playingList; +// _repeatMode = state.repeatMode; +// _generateShuffleList(); +// if (state.playingTrack != null) { +// _playTrack(state.playingTrack!, playWhenReady: false); +// } +// setVolume(state.volume); +// notifyPlayStateChanged(); +// } +// } diff --git a/lib/media/tracks/tracks_player_impl_mobile.dart b/lib/media/tracks/tracks_player_impl_mobile.dart new file mode 100644 index 0000000..2285b5e --- /dev/null +++ b/lib/media/tracks/tracks_player_impl_mobile.dart @@ -0,0 +1,369 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:music_player/music_player.dart'; + +import '../../model/persistence_player_state.dart'; +import '../../repository.dart'; +import '../../utils/cache/cached_image.dart'; +import '../../utils/media_cache/media_cache.dart'; +import 'track_list.dart'; +import 'tracks_player.dart'; + +extension _Metadata on MusicMetadata { + Track toTrack() { + final List> artists; + final Map? album; + + if (extras != null) { + artists = (jsonDecode(extras!['artists']) as List).cast(); + album = jsonDecode(extras!['album']) as Map?; + } else { + artists = const []; + album = null; + } + return Track( + id: int.parse(mediaId), + name: title ?? '', + uri: mediaUri, + artists: artists.map(ArtistMini.fromJson).toList(), + album: album == null ? null : AlbumMini.fromJson(album), + imageUrl: extras?['imageUrl'] as String, + duration: Duration(milliseconds: duration), + type: TrackType.values.byName(extras?['fee']), + isRecommend: extras?['isRecommend'] == true, + ); + } +} + +extension _Track on Track { + MusicMetadata toMetadata() { + return MusicMetadata( + mediaId: id.toString(), + title: name, + mediaUri: uri, + subtitle: '$name - ${artists.map((e) => e.name).join('/')}', + duration: duration.inMilliseconds, + iconUri: imageUrl, + extras: { + 'album': jsonEncode(album?.toJson()), + 'artists': jsonEncode(artists.map((e) => e.toJson()).toList()), + 'imageUrl': imageUrl, + 'fee': type.name, + 'isRecommend': isRecommend, + }, + ); + } +} + +extension _TrackList on TrackList { + PlayQueue toPlayQueue() { + return PlayQueue( + queueId: id, + queueTitle: 'play_list', + queue: tracks.map((e) => e.toMetadata()).toList(), + extras: { + 'isUserFavoriteList': isUserFavoriteList, + 'rawPlaylistId': rawPlaylistId, + }, + ); + } +} + +extension _PlayQueue on PlayQueue { + TrackList toTrackList() { + if (queueId == kFmTrackListId) { + return TrackList.fm(tracks: queue.map((e) => e.toTrack()).toList()); + } + return TrackList.playlist( + id: queueId, + tracks: queue.map((e) => e.toTrack()).toList(), + isUserFavoriteList: extras?['isUserFavoriteList'] as bool? ?? false, + rawPlaylistId: extras?['rawPlaylistId'] as int?, + ); + } +} + +const _playModeHeart = 3; + +class TracksPlayerImplMobile extends TracksPlayer { + TracksPlayerImplMobile() { + _player.metadataListenable.addListener(notifyPlayStateChanged); + _player.playbackStateListenable.addListener(notifyPlayStateChanged); + _player.playModeListenable.addListener(notifyPlayStateChanged); + } + + final _player = MusicPlayer(); + + @override + Duration? get bufferedPosition => + Duration(milliseconds: _player.playbackState.bufferedPosition); + + @override + Track? get current => _player.metadata?.toTrack(); + + @override + Duration? get duration { + final d = _player.metadata?.duration; + if (d == null) { + return null; + } + return Duration(milliseconds: d); + } + + @override + Future getNextTrack() async { + final current = _player.metadata; + if (current == null) { + return null; + } + final next = await _player.getNextMusic(current); + return next.toTrack(); + } + + @override + Future getPreviousTrack() async { + final current = _player.metadata; + if (current == null) { + return null; + } + final previous = await _player.getPreviousMusic(current); + return previous.toTrack(); + } + + @override + Future insertToNext(Track track) async { + await _player.insertToNext(track.toMetadata()); + } + + @override + bool get isPlaying => + _player.value.playbackState.state == PlayerState.playing; + + @override + Future pause() async { + await _player.transportControls.pause(); + } + + @override + Future play() { + return _player.transportControls.play(); + } + + @override + Future playFromMediaId(int trackId, {bool play = true}) async { + if (play) { + await _player.transportControls.playFromMediaId(trackId.toString()); + } else { + await _player.transportControls.prepareFromMediaId(trackId.toString()); + } + } + + @override + double get playbackSpeed => _player.playbackState.speed; + + @override + Duration? get position { + final p = _player.playbackState.computedPosition; + return Duration(milliseconds: p); + } + + @override + RepeatMode get repeatMode { + final playMode = _player.playMode; + if (playMode == PlayMode.sequence) { + return RepeatMode.sequence; + } else if (playMode == PlayMode.shuffle) { + return RepeatMode.shuffle; + } else if (playMode == PlayMode.single) { + return RepeatMode.single; + } else if (playMode.index == _playModeHeart) { + return RepeatMode.heart; + } else { + assert(false, 'unknown play mode: $playMode'); + return RepeatMode.sequence; + } + } + + @override + Future seekTo(Duration position) async { + await _player.transportControls.seekTo(position.inMilliseconds); + } + + @override + Future setPlaybackSpeed(double speed) { + return _player.transportControls.setPlaybackSpeed(speed); + } + + @override + Future setRepeatMode(RepeatMode repeatMode) async { + final PlayMode playMode; + switch (repeatMode) { + case RepeatMode.shuffle: + playMode = PlayMode.shuffle; + break; + case RepeatMode.single: + playMode = PlayMode.single; + break; + case RepeatMode.sequence: + playMode = PlayMode.sequence; + break; + case RepeatMode.heart: + playMode = PlayMode.undefined(_playModeHeart); + break; + } + await _player.transportControls.setPlayMode(playMode); + } + + @override + void setTrackList(TrackList trackList) { + _player.setPlayQueue(trackList.toPlayQueue()); + } + + @override + Future setVolume(double volume) async { + // no need to implement + } + + @override + bool get canAdjustVolume => false; + + @override + Future skipToNext() { + return _player.transportControls.skipToNext(); + } + + @override + Future skipToPrevious() { + return _player.transportControls.skipToPrevious(); + } + + @override + Future stop() { + // FIXME stop impl + return _player.transportControls.pause(); + } + + @override + TrackList get trackList => _player.queue.toTrackList(); + + @override + double get volume => 1; + + @override + bool get isBuffering => _player.playbackState.state == PlayerState.buffering; + + @override + Future restoreFromPersistence(PersistencePlayerState state) async { + final isServiceRunning = await _player.isMusicServiceAvailable(); + if (isServiceRunning) { + d('service running, skip restore'); + return; + } + await _player.setPlayQueue(state.playingList.toPlayQueue()); + await setRepeatMode(state.repeatMode); + if (state.playingTrack != null) { + await _player.transportControls + .prepareFromMediaId(state.playingTrack!.id.toString()); + } + } +} + +void runMobileBackgroundService() { + runBackgroundService( + imageLoadInterceptor: _loadImageInterceptor, + playUriInterceptor: _playUriInterceptor, + playQueueInterceptor: _PlayQueueInterceptor(), + ); +} + +// 获取播放地址 +Future _playUriInterceptor(String? mediaId, String? fallbackUri) async { + final trackId = int.parse(mediaId!); + // final result = await neteaseRepository!.getPlayUrl(trackId); + // + // if (result.isError) { + // e('get play url error: ${result.asError!.error}'); + // } + // + // final url = result.isError + // ? fallbackUri + // : result.asValue!.value.replaceFirst('http://', 'https://'); + final url = "aaaa"; + if (url == null) { + return ''; + } + final proxyUrl = await generateTrackProxyUrl(trackId, url); + d('play url: $proxyUrl'); + return proxyUrl; +} + +Future _loadImageInterceptor(MusicMetadata metadata) async { + final result = await loadImageFromOtherIsolate(metadata.iconUri); + if (result == null) { + e('load image error: ${metadata.iconUri}'); + throw Exception('load image error'); + } + d('load image complete ${metadata.iconUri} ${result.length}'); + return result; +} + +class _PlayQueueInterceptor extends PlayQueueInterceptor { + @override + Future onPlayNextNoMoreMusic( + BackgroundPlayQueue queue, + PlayMode playMode, + ) async { + if (playMode.index == _playModeHeart) { + final current = player!.metadata?.mediaId; + if (current == null) { + return null; + } + var index = + queue.queue.indexWhere((element) => element.mediaId == current) + 1; + if (index >= queue.queue.length) { + index = 0; + } + return queue.queue[index]; + } + return super.onPlayNextNoMoreMusic(queue, playMode); + } + + @override + Future onPlayPreviousNoMoreMusic( + BackgroundPlayQueue queue, + PlayMode playMode, + ) { + if (playMode.index == _playModeHeart) { + final current = player!.metadata?.mediaId; + if (current == null) { + return Future.value(queue.queue.last); + } + var index = + queue.queue.indexWhere((element) => element.mediaId == current) - 1; + if (index < 0) { + index = queue.queue.length - 1; + } + return Future.value(queue.queue[index]); + } + return super.onPlayPreviousNoMoreMusic(queue, playMode); + } + + @override + Future> fetchMoreMusic( + BackgroundPlayQueue queue, + PlayMode playMode, + ) async { + if (queue.queueId == kFmTrackListId) { + return []; + // final musics = await neteaseRepository!.getPersonalFmMusics(); + // if (musics.isError) { + // return []; + // } + // return musics.asValue!.value.map((m) => m.toMetadata()).toList(); + } + return super.fetchMoreMusic(queue, playMode); + } +} diff --git a/lib/model/persistence_player_state.dart b/lib/model/persistence_player_state.dart new file mode 100644 index 0000000..46ec102 --- /dev/null +++ b/lib/model/persistence_player_state.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../media/tracks/track_list.dart'; +import '../media/tracks/tracks_player.dart'; +import '../repository/data/track.dart'; + +part 'persistence_player_state.g.dart'; + +@JsonSerializable() +class PersistencePlayerState with EquatableMixin { + PersistencePlayerState({ + required this.volume, + required this.playingTrack, + required this.playingList, + required this.repeatMode, + }); + + factory PersistencePlayerState.fromJson(Map json) => + _$PersistencePlayerStateFromJson(json); + + final double volume; + final Track? playingTrack; + final TrackList playingList; + final RepeatMode repeatMode; + + @override + List get props => [ + volume, + playingTrack, + playingList, + repeatMode, + ]; + + Map toJson() => _$PersistencePlayerStateToJson(this); +} diff --git a/lib/model/persistence_player_state.g.dart b/lib/model/persistence_player_state.g.dart new file mode 100644 index 0000000..d741195 --- /dev/null +++ b/lib/model/persistence_player_state.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'persistence_player_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PersistencePlayerState _$PersistencePlayerStateFromJson(Map json) => + PersistencePlayerState( + volume: (json['volume'] as num).toDouble(), + playingTrack: json['playingTrack'] == null + ? null + : Track.fromJson( + Map.from(json['playingTrack'] as Map)), + playingList: TrackList.fromJson( + Map.from(json['playingList'] as Map)), + repeatMode: $enumDecode(_$RepeatModeEnumMap, json['repeatMode']), + ); + +Map _$PersistencePlayerStateToJson( + PersistencePlayerState instance) => + { + 'volume': instance.volume, + 'playingTrack': instance.playingTrack?.toJson(), + 'playingList': instance.playingList.toJson(), + 'repeatMode': _$RepeatModeEnumMap[instance.repeatMode]!, + }; + +const _$RepeatModeEnumMap = { + RepeatMode.shuffle: 'shuffle', + RepeatMode.single: 'single', + RepeatMode.sequence: 'sequence', + RepeatMode.heart: 'heart', +}; diff --git a/lib/model/region_flag.dart b/lib/model/region_flag.dart new file mode 100644 index 0000000..add7952 --- /dev/null +++ b/lib/model/region_flag.dart @@ -0,0 +1,52 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +part 'region_flag.g.dart'; + +@JsonSerializable() +@immutable +class RegionFlag { + const RegionFlag({ + required this.code, + required this.emoji, + required this.unicode, + required this.name, + this.dialCode, + }); + + factory RegionFlag.fromMap(Map map) => _$RegionFlagFromJson(map); + + final String code; + final String emoji; + final String unicode; + final String name; + + // could be null + final String? dialCode; + + Map toMap() => _$RegionFlagToJson(this); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RegionFlag && + runtimeType == other.runtimeType && + code == other.code && + emoji == other.emoji && + unicode == other.unicode && + name == other.name && + dialCode == other.dialCode; + + @override + int get hashCode => + code.hashCode ^ + emoji.hashCode ^ + unicode.hashCode ^ + name.hashCode ^ + dialCode.hashCode; + + @override + String toString() { + return 'RegionFlag{code: $code, emoji: $emoji, unicode: $unicode, name: $name, dialCode: $dialCode}'; + } +} diff --git a/lib/model/region_flag.g.dart b/lib/model/region_flag.g.dart new file mode 100644 index 0000000..c1e2f22 --- /dev/null +++ b/lib/model/region_flag.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'region_flag.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RegionFlag _$RegionFlagFromJson(Map json) => RegionFlag( + code: json['code'] as String, + emoji: json['emoji'] as String, + unicode: json['unicode'] as String, + name: json['name'] as String, + dialCode: json['dialCode'] as String?, + ); + +Map _$RegionFlagToJson(RegionFlag instance) => + { + 'code': instance.code, + 'emoji': instance.emoji, + 'unicode': instance.unicode, + 'name': instance.name, + 'dialCode': instance.dialCode, + }; diff --git a/lib/navigation/app.dart b/lib/navigation/app.dart new file mode 100644 index 0000000..c73ae33 --- /dev/null +++ b/lib/navigation/app.dart @@ -0,0 +1,33 @@ + +import 'package:EOEFANS/navigation/common/material/app.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../utils/platform_configuration.dart'; +import 'common/material/theme.dart'; + +class FansApp extends ConsumerWidget { + const FansApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return MaterialApp( + title: 'EOEFANS', + // supportedLocales: S.delegate.supportedLocales, + theme: lightTheme, + darkTheme: darkTheme, + // themeMode: ref.watch( + // settingStateProvider.select((value) => value.themeMode), + // ), + debugShowCheckedModeBanner: false, + builder: (context, child) { + return AppTheme( + child: AppPlatformConfiguration( + child: CopyRightOverlay(child: child), + ), + ); + }, + ); + } +} diff --git a/lib/navigation/common/material/app.dart b/lib/navigation/common/material/app.dart new file mode 100644 index 0000000..75d83a0 --- /dev/null +++ b/lib/navigation/common/material/app.dart @@ -0,0 +1,84 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../../../extension.dart'; +import '../../../providers/settings_provider.dart'; + +class CopyRightOverlay extends HookConsumerWidget { + const CopyRightOverlay({super.key, this.child}); + + final Widget? child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final copyRight = context.strings.copyRightOverlay; + final textStyle = context.textTheme.bodySmall!.copyWith( + color: context.textTheme.bodySmall!.color!.withOpacity(0.3), + ); + final painter = useMemoized( + () => _CopyrightPainter(copyright: copyRight, style: textStyle), + [copyRight, textStyle], + ); + return CustomPaint( + foregroundPainter: + ref.watch(settingStateProvider.select((value) => value.copyright)) + ? null + : painter, + child: child, + ); + } +} + +class _CopyrightPainter extends CustomPainter { + _CopyrightPainter({ + required String copyright, + required TextStyle style, + }) : _textPainter = TextPainter( + text: TextSpan( + text: copyright, + style: style, + ), + textDirection: TextDirection.ltr, + ); + + final TextPainter _textPainter; + + bool _dirty = true; + + void setText(String text, TextStyle style) { + _textPainter.text = TextSpan( + text: text, + style: style, + ); + _dirty = true; + } + + @override + void paint(Canvas canvas, Size size) { + const radius = math.pi / 4; + if (_dirty) { + _textPainter.layout(); + _dirty = false; + } + canvas.rotate(-radius); + canvas.translate(-size.width, 0); + + var dy = 0.0; + while (dy < size.height * 1.5) { + var dx = 0.0; + while (dx < size.width * 1.5) { + _textPainter.paint(canvas, Offset(dx, dy)); + dx += _textPainter.width * 1.5; + } + dy += _textPainter.height * 3; + dx = 0; + } + } + + @override + bool shouldRepaint(_CopyrightPainter oldDelegate) { + return _textPainter != oldDelegate._textPainter; + } +} diff --git a/lib/navigation/common/material/theme.dart b/lib/navigation/common/material/theme.dart new file mode 100644 index 0000000..fb91042 --- /dev/null +++ b/lib/navigation/common/material/theme.dart @@ -0,0 +1,197 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +import '../../../../utils/system/system_fonts.dart'; + +const lightSwatch = MaterialColor(0xFFdd4237, { + 900: Color(0xFFae2a20), + 800: Color(0xFFbe332a), + 700: Color(0xFFcb3931), + 600: Color(0xFFdd4237), + 500: Color(0xFFec4b38), + 400: Color(0xFFe85951), + 300: Color(0xFFdf7674), + 200: Color(0xFFea9c9a), + 100: Color(0xFFfcced2), + 50: Color(0xFFfeebee), +}); + +ThemeData get darkTheme { + final theme = ThemeData.from( + colorScheme: ColorScheme.dark( + background: Color.alphaBlend(Colors.black87, Colors.white), + onBackground: Color.alphaBlend(Colors.white54, Colors.black), + surface: Color.alphaBlend(Colors.black87, Colors.white), + onSurface: Color.alphaBlend(Colors.white70, Colors.black), + primary: lightSwatch, + secondary: lightSwatch[300]!, + tertiary: lightSwatch[100], + onPrimary: const Color(0xFFDDDDDD), + ), + ); + return theme + .copyWith( + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(milliseconds: 1000), + ), + ) + .withFallbackFonts() + .applyCommon(); +} + +ThemeData get lightTheme => _buildTheme(lightSwatch); + +ThemeData _buildTheme(Color primaryColor) { + final theme = ThemeData.from( + colorScheme: const ColorScheme.light( + primary: lightSwatch, + ), + ); + return theme + .copyWith( + tooltipTheme: const TooltipThemeData( + waitDuration: Duration(milliseconds: 1000), + ), + iconTheme: IconThemeData( + color: theme.iconTheme.color!.withOpacity(0.7), + size: 24, + ), + scaffoldBackgroundColor: const Color(0xFFEEEEEE), + ) + .withFallbackFonts() + .applyCommon(); +} + +extension QuietAppTheme on BuildContext { + TextTheme get textTheme => Theme.of(this).textTheme; + + TextTheme get primaryTextTheme => Theme.of(this).primaryTextTheme; + + AppColorScheme get colorScheme => AppTheme.colorScheme(this); +} + +extension TextStyleExtesntion on TextStyle? { + TextStyle? get bold => this?.copyWith(fontWeight: FontWeight.bold); +} + +extension _ThemeExt on ThemeData { + ThemeData applyCommon() { + return copyWith( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: ZoomPageTransitionsBuilder(), + TargetPlatform.linux: ZoomPageTransitionsBuilder(), + TargetPlatform.windows: ZoomPageTransitionsBuilder(), + }, + ), + ); + } +} + +class AppColorScheme with EquatableMixin { + AppColorScheme({ + required this.brightness, + required this.background, + required this.backgroundSecondary, + required this.primary, + required this.textPrimary, + required this.textHint, + required this.textDisabled, + required this.onPrimary, + required this.surface, + required this.highlight, + required this.divider, + }); + + final Brightness brightness; + + final Color background; + final Color backgroundSecondary; + + final Color primary; + + final Color textPrimary; + final Color textHint; + final Color textDisabled; + + final Color onPrimary; + + final Color surface; + final Color highlight; + final Color divider; + + Color surfaceWithElevation(double elevation) => brightness == Brightness.light + ? surface + : ElevationOverlay.colorWithOverlay(surface, textPrimary, elevation); + + @override + List get props => [ + brightness, + background, + backgroundSecondary, + primary, + textPrimary, + textHint, + textDisabled, + onPrimary, + surface, + highlight, + divider, + ]; +} + +class AppTheme extends StatelessWidget { + const AppTheme({super.key, required this.child}); + + final Widget child; + + static AppColorScheme colorScheme(BuildContext context) => + _AppThemeData.of(context).colorScheme; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final colorScheme = AppColorScheme( + brightness: theme.brightness, + background: theme.colorScheme.background, + backgroundSecondary: theme.scaffoldBackgroundColor, + primary: theme.colorScheme.primary, + textPrimary: theme.textTheme.bodyMedium!.color!, + textHint: theme.textTheme.bodySmall!.color!, + textDisabled: theme.disabledColor, + onPrimary: theme.colorScheme.onPrimary, + surface: theme.colorScheme.surface, + highlight: theme.highlightColor, + divider: theme.dividerColor, + ); + + return _AppThemeData( + colorScheme: colorScheme, + child: child, + ); + } +} + +class _AppThemeData extends InheritedWidget { + const _AppThemeData({ + super.key, + required super.child, + required this.colorScheme, + }); + + final AppColorScheme colorScheme; + + static _AppThemeData of(BuildContext context) { + final result = context.dependOnInheritedWidgetOfExactType<_AppThemeData>(); + assert(result != null, 'No _AppTheme found in context'); + return result!; + } + + @override + bool updateShouldNotify(_AppThemeData old) { + return colorScheme != old.colorScheme; + } +} diff --git a/lib/navigation/common/page_splash.dart b/lib/navigation/common/page_splash.dart new file mode 100644 index 0000000..4f0d6f9 --- /dev/null +++ b/lib/navigation/common/page_splash.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +// import '../../providers/account_provider.dart'; + +///used to build application widget +///[data] the data initial in [PageSplash] +typedef AppBuilder = Widget Function(BuildContext context, List data); + +///the splash screen of application +class PageSplash extends ConsumerStatefulWidget { + const PageSplash({super.key, required this.futures, required this.builder}); + + ///the data need init before application running + final List futures; + + final AppBuilder builder; + + @override + ConsumerState createState() => _PageSplashState(); +} + +class _PageSplashState extends ConsumerState { + List? _data; + + @override + void initState() { + super.initState(); + // final tasks = [ + // ref.read(userProvider.notifier).initialize(), + // ]; + // final start = DateTime.now().millisecondsSinceEpoch; + // Future.wait([ + // ...widget.futures, + // ...tasks, + // ]).then((data) { + // final duration = DateTime.now().millisecondsSinceEpoch - start; + // debugPrint('flutter initial in : $duration'); + // setState(() { + // _data = data; + // }); + // }); + } + + @override + Widget build(BuildContext context) { + if (_data == null) { + return const ColoredBox(color: Color(0xFFd92e29)); + } + return widget.builder(context, _data!); + } +} diff --git a/lib/providers/preference_provider.dart b/lib/providers/preference_provider.dart new file mode 100644 index 0000000..2ec06fe --- /dev/null +++ b/lib/providers/preference_provider.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../model/persistence_player_state.dart'; + +final sharedPreferenceProvider = Provider( + (ref) => throw UnimplementedError('init with override'), +); + +extension SharedPreferenceProviderExtension on SharedPreferences { + Size? getWindowSize() { + final width = getDouble('window_width') ?? 0; + final height = getDouble('window_height') ?? 0; + if (width == 0 || height == 0) { + return null; + } + return Size(width, height); + } + + Future setWindowSize(Size size) async { + await Future.wait([ + setDouble('window_width', size.width), + setDouble('window_height', size.height), + ]); + } + + Future getPlayerState() async { + final json = getString('player_state'); + if (json == null) { + return null; + } + try { + final map = await compute(jsonDecode, json); + return PersistencePlayerState.fromJson(map); + } catch (error, stacktrace) { + debugPrint('getPlayerState error: $error\n$stacktrace'); + return null; + } + } + + Future setPlayerState(PersistencePlayerState state) async { + final json = await compute(jsonEncode, state.toJson()); + await setString('player_state', json); + } + + Future isLoginByQrCode() async { + return getBool('login_by_qr_code') ?? false; + } + + // ignore: avoid_positional_boolean_parameters + Future setLoginByQrCode(bool value) async { + await setBool('login_by_qr_code', value); + } + + // Future getLoginUser() async { + // final json = getString('login_user'); + // if (json == null) { + // return null; + // } + // try { + // final map = await compute(jsonDecode, json); + // return User.fromJson(map); + // } catch (error, stacktrace) { + // debugPrint('getLoginUser error: $error\n$stacktrace'); + // return null; + // } + // } + // + // Future setLoginUser(User? user) async { + // if (user == null) { + // await remove('login_user'); + // } else { + // await setString('login_user', jsonEncode(user.toJson())); + // } + // } +} diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart new file mode 100644 index 0000000..5a2904f --- /dev/null +++ b/lib/providers/settings_provider.dart @@ -0,0 +1,94 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'preference_provider.dart'; + +const String _prefix = 'quiet:settings:'; + +const String _keyThemeMode = '$_prefix:themeMode'; + +const String _keyCopyright = '$_prefix:copyright'; + +const String _keySkipWelcomePage = '$_prefix:skipWelcomePage'; + +final settingStateProvider = StateNotifierProvider( + (ref) { + return Settings(ref.read(sharedPreferenceProvider)); + }, +); + +class SettingState with EquatableMixin { + const SettingState({ + required this.themeMode, + required this.skipWelcomePage, + required this.copyright, + required this.skipAccompaniment, + }); + + factory SettingState.fromPreference(SharedPreferences preference) { + final mode = preference.getInt(_keyThemeMode) ?? 0; + assert(mode >= 0 && mode < ThemeMode.values.length, 'invalid theme mode'); + return SettingState( + themeMode: ThemeMode.values[mode.clamp(0, ThemeMode.values.length - 1)], + skipWelcomePage: preference.getBool(_keySkipWelcomePage) ?? false, + copyright: preference.getBool(_keyCopyright) ?? false, + skipAccompaniment: + preference.getBool('$_prefix:skipAccompaniment') ?? false, + ); + } + + final ThemeMode themeMode; + final bool skipWelcomePage; + final bool copyright; + final bool skipAccompaniment; + + @override + List get props => [ + themeMode, + skipWelcomePage, + copyright, + skipAccompaniment, + ]; + + SettingState copyWith({ + ThemeMode? themeMode, + bool? skipWelcomePage, + bool? copyright, + bool? skipAccompaniment, + }) => + SettingState( + themeMode: themeMode ?? this.themeMode, + skipWelcomePage: skipWelcomePage ?? this.skipWelcomePage, + copyright: copyright ?? this.copyright, + skipAccompaniment: skipAccompaniment ?? this.skipAccompaniment, + ); +} + +class Settings extends StateNotifier { + Settings(this._preferences) + : super(SettingState.fromPreference(_preferences)); + + final SharedPreferences _preferences; + + void setThemeMode(ThemeMode themeMode) { + _preferences.setInt(_keyThemeMode, themeMode.index); + state = state.copyWith(themeMode: themeMode); + } + + void setShowCopyrightOverlay({required bool show}) { + _preferences.setBool(_keyCopyright, show); + state = state.copyWith(copyright: show); + } + + void setSkipWelcomePage() { + _preferences.setBool(_keySkipWelcomePage, true); + state = state.copyWith(skipWelcomePage: true); + } + + void setSkipAccompaniment({required bool skip}) { + _preferences.setBool('$_prefix:skipAccompaniment', skip); + state = state.copyWith(skipAccompaniment: skip); + } +} diff --git a/lib/repository.dart b/lib/repository.dart new file mode 100644 index 0000000..da60175 --- /dev/null +++ b/lib/repository.dart @@ -0,0 +1,13 @@ +export 'repository/data/album_detail.dart'; +export 'repository/data/artist_detail.dart'; +export 'repository/data/cloud_tracks_detail.dart'; +export 'repository/data/play_record.dart'; +export 'repository/data/playlist_detail.dart'; +export 'repository/data/recommended_playlist.dart'; +export 'repository/data/track.dart'; +export 'repository/data/user.dart'; +export 'repository/local_cache_data.dart'; +export 'repository/netease.dart'; +// export 'repository/network_repository.dart'; + +void importRepository() {} diff --git a/lib/repository/app_dir.dart b/lib/repository/app_dir.dart new file mode 100644 index 0000000..6a129d9 --- /dev/null +++ b/lib/repository/app_dir.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; + +late final Directory appDir; + +Directory get cacheDir => Directory(join(appDir.path, 'cache')); + +Future initAppDir() async { + if (Platform.isLinux) { + final home = Platform.environment['HOME']; + assert(home != null, 'HOME is null'); + appDir = Directory(join(home!, '.quiet')); + return; + } + if (Platform.isWindows) { + appDir = await getApplicationSupportDirectory(); + return; + } + appDir = await getApplicationDocumentsDirectory(); +} diff --git a/lib/repository/data/album_detail.dart b/lib/repository/data/album_detail.dart new file mode 100644 index 0000000..1f316ff --- /dev/null +++ b/lib/repository/data/album_detail.dart @@ -0,0 +1,89 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'track.dart'; + +part 'album_detail.g.dart'; + +@JsonSerializable() +class AlbumDetail { + AlbumDetail({ + required this.album, + required this.tracks, + }); + + factory AlbumDetail.fromJson(Map json) => + _$AlbumDetailFromJson(json); + + final Album album; + + final List tracks; + + Map toJson() => _$AlbumDetailToJson(this); +} + +@JsonSerializable() +class Album with EquatableMixin { + Album({ + required this.name, + required this.id, + required this.briefDesc, + required this.publishTime, + required this.company, + required this.picUrl, + required this.description, + required this.artist, + required this.paid, + required this.onSale, + required this.size, + required this.liked, + required this.commentCount, + required this.likedCount, + required this.shareCount, + }); + + factory Album.fromJson(Map json) => _$AlbumFromJson(json); + + final String name; + final int id; + + final String briefDesc; + final DateTime publishTime; + final String company; + final String picUrl; + + final String description; + + final ArtistMini artist; + + final bool paid; + final bool onSale; + + final int size; + + final bool liked; + final int commentCount; + final int likedCount; + final int shareCount; + + @override + List get props => [ + name, + id, + briefDesc, + publishTime, + company, + picUrl, + description, + artist, + paid, + onSale, + size, + liked, + commentCount, + likedCount, + shareCount, + ]; + + Map toJson() => _$AlbumToJson(this); +} diff --git a/lib/repository/data/album_detail.g.dart b/lib/repository/data/album_detail.g.dart new file mode 100644 index 0000000..7d86eee --- /dev/null +++ b/lib/repository/data/album_detail.g.dart @@ -0,0 +1,57 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'album_detail.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AlbumDetail _$AlbumDetailFromJson(Map json) => AlbumDetail( + album: Album.fromJson(Map.from(json['album'] as Map)), + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(Map.from(e as Map))) + .toList(), + ); + +Map _$AlbumDetailToJson(AlbumDetail instance) => + { + 'album': instance.album.toJson(), + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), + }; + +Album _$AlbumFromJson(Map json) => Album( + name: json['name'] as String, + id: json['id'] as int, + briefDesc: json['briefDesc'] as String, + publishTime: DateTime.parse(json['publishTime'] as String), + company: json['company'] as String, + picUrl: json['picUrl'] as String, + description: json['description'] as String, + artist: + ArtistMini.fromJson(Map.from(json['artist'] as Map)), + paid: json['paid'] as bool, + onSale: json['onSale'] as bool, + size: json['size'] as int, + liked: json['liked'] as bool, + commentCount: json['commentCount'] as int, + likedCount: json['likedCount'] as int, + shareCount: json['shareCount'] as int, + ); + +Map _$AlbumToJson(Album instance) => { + 'name': instance.name, + 'id': instance.id, + 'briefDesc': instance.briefDesc, + 'publishTime': instance.publishTime.toIso8601String(), + 'company': instance.company, + 'picUrl': instance.picUrl, + 'description': instance.description, + 'artist': instance.artist.toJson(), + 'paid': instance.paid, + 'onSale': instance.onSale, + 'size': instance.size, + 'liked': instance.liked, + 'commentCount': instance.commentCount, + 'likedCount': instance.likedCount, + 'shareCount': instance.shareCount, + }; diff --git a/lib/repository/data/artist_detail.dart b/lib/repository/data/artist_detail.dart new file mode 100644 index 0000000..5aab422 --- /dev/null +++ b/lib/repository/data/artist_detail.dart @@ -0,0 +1,73 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'track.dart'; + +part 'artist_detail.g.dart'; + +class ArtistDetail { + ArtistDetail({ + required this.hotSongs, + required this.more, + required this.artist, + }); + + final List hotSongs; + + final bool more; + + final Artist artist; +} + +@JsonSerializable() +class Artist with EquatableMixin { + Artist({ + required this.name, + required this.id, + required this.publishTime, + required this.image1v1Url, + required this.picUrl, + required this.albumSize, + required this.mvSize, + required this.musicSize, + required this.followed, + required this.briefDesc, + required this.alias, + }); + + factory Artist.fromJson(Map json) => _$ArtistFromJson(json); + + final String name; + final int id; + + final int publishTime; + final String image1v1Url; + final String picUrl; + + final int albumSize; + final int mvSize; + final int musicSize; + + final bool followed; + + final String briefDesc; + + final List alias; + + @override + List get props => [ + name, + id, + publishTime, + image1v1Url, + picUrl, + albumSize, + mvSize, + musicSize, + followed, + briefDesc, + alias, + ]; + + Map toJson() => _$ArtistToJson(this); +} diff --git a/lib/repository/data/artist_detail.g.dart b/lib/repository/data/artist_detail.g.dart new file mode 100644 index 0000000..3471ba3 --- /dev/null +++ b/lib/repository/data/artist_detail.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'artist_detail.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Artist _$ArtistFromJson(Map json) => Artist( + name: json['name'] as String, + id: json['id'] as int, + publishTime: json['publishTime'] as int, + image1v1Url: json['image1v1Url'] as String, + picUrl: json['picUrl'] as String, + albumSize: json['albumSize'] as int, + mvSize: json['mvSize'] as int, + musicSize: json['musicSize'] as int, + followed: json['followed'] as bool, + briefDesc: json['briefDesc'] as String, + alias: (json['alias'] as List).map((e) => e as String).toList(), + ); + +Map _$ArtistToJson(Artist instance) => { + 'name': instance.name, + 'id': instance.id, + 'publishTime': instance.publishTime, + 'image1v1Url': instance.image1v1Url, + 'picUrl': instance.picUrl, + 'albumSize': instance.albumSize, + 'mvSize': instance.mvSize, + 'musicSize': instance.musicSize, + 'followed': instance.followed, + 'briefDesc': instance.briefDesc, + 'alias': instance.alias, + }; diff --git a/lib/repository/data/cloud_tracks_detail.dart b/lib/repository/data/cloud_tracks_detail.dart new file mode 100644 index 0000000..67bdc99 --- /dev/null +++ b/lib/repository/data/cloud_tracks_detail.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +import 'track.dart'; + +part 'cloud_tracks_detail.g.dart'; + +@JsonSerializable() +class CloudTracksDetail { + CloudTracksDetail({ + required this.tracks, + required this.size, + required this.maxSize, + required this.trackCount, + }); + + factory CloudTracksDetail.fromJson(Map json) => + _$CloudTracksDetailFromJson(json); + + final List tracks; + final int size; + final int maxSize; + final int trackCount; + + Map toJson() => _$CloudTracksDetailToJson(this); +} diff --git a/lib/repository/data/cloud_tracks_detail.g.dart b/lib/repository/data/cloud_tracks_detail.g.dart new file mode 100644 index 0000000..48330e7 --- /dev/null +++ b/lib/repository/data/cloud_tracks_detail.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cloud_tracks_detail.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CloudTracksDetail _$CloudTracksDetailFromJson(Map json) => CloudTracksDetail( + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(Map.from(e as Map))) + .toList(), + size: json['size'] as int, + maxSize: json['maxSize'] as int, + trackCount: json['trackCount'] as int, + ); + +Map _$CloudTracksDetailToJson(CloudTracksDetail instance) => + { + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), + 'size': instance.size, + 'maxSize': instance.maxSize, + 'trackCount': instance.trackCount, + }; diff --git a/lib/repository/data/login_qr_key_status.dart b/lib/repository/data/login_qr_key_status.dart new file mode 100644 index 0000000..0cc076a --- /dev/null +++ b/lib/repository/data/login_qr_key_status.dart @@ -0,0 +1,6 @@ +enum LoginQrKeyStatus { + expired, + waitingScan, + waitingConfirm, + confirmed, +} diff --git a/lib/repository/data/play_record.dart b/lib/repository/data/play_record.dart new file mode 100644 index 0000000..40ff51b --- /dev/null +++ b/lib/repository/data/play_record.dart @@ -0,0 +1,30 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import '../../repository.dart'; + +part 'play_record.g.dart'; + +@JsonSerializable() +class PlayRecord with EquatableMixin { + const PlayRecord({ + required this.playCount, + required this.score, + required this.song, + }); + + factory PlayRecord.fromJson(Map json) => + _$PlayRecordFromJson(json); + + final int playCount; + final int score; + final Track song; + + Map toJson() => _$PlayRecordToJson(this); + + @override + List get props => [ + playCount, + score, + song, + ]; +} diff --git a/lib/repository/data/play_record.g.dart b/lib/repository/data/play_record.g.dart new file mode 100644 index 0000000..bcb868b --- /dev/null +++ b/lib/repository/data/play_record.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'play_record.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PlayRecord _$PlayRecordFromJson(Map json) => PlayRecord( + playCount: json['playCount'] as int, + score: json['score'] as int, + song: Track.fromJson(Map.from(json['song'] as Map)), + ); + +Map _$PlayRecordToJson(PlayRecord instance) => + { + 'playCount': instance.playCount, + 'score': instance.score, + 'song': instance.song.toJson(), + }; diff --git a/lib/repository/data/playlist_detail.dart b/lib/repository/data/playlist_detail.dart new file mode 100644 index 0000000..a51e725 --- /dev/null +++ b/lib/repository/data/playlist_detail.dart @@ -0,0 +1,141 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'track.dart'; + +import 'user.dart'; + +part 'playlist_detail.g.dart'; + +@JsonSerializable() +@HiveType(typeId: 1) +class PlaylistDetail with EquatableMixin { + PlaylistDetail({ + required this.id, + required this.tracks, + required this.creator, + required this.coverUrl, + required this.trackCount, + required this.subscribed, + required this.subscribedCount, + required this.shareCount, + required this.playCount, + required this.trackUpdateTime, + required this.name, + required this.description, + required this.commentCount, + required this.trackIds, + required this.createTime, + required this.isFavorite, + }); + + factory PlaylistDetail.fromJson(Map json) => + _$PlaylistDetailFromJson(json); + + @HiveField(0) + final int id; + + @HiveField(1) + final List tracks; + + @HiveField(2) + final User creator; + + @HiveField(3) + final String coverUrl; + + @HiveField(4) + final int trackCount; + + @HiveField(5) + final bool subscribed; + + @HiveField(6) + final int subscribedCount; + + @HiveField(7) + final int shareCount; + + @HiveField(8) + final int playCount; + + @HiveField(9) + final int trackUpdateTime; + + @HiveField(10) + final String name; + + @HiveField(11) + final String description; + + @HiveField(12) + final int commentCount; + + @HiveField(13) + final List trackIds; + + @HiveField(14) + final DateTime createTime; + + @HiveField(15) + final bool isFavorite; + + @override + List get props => [ + id, + tracks, + creator, + coverUrl, + trackCount, + subscribed, + subscribedCount, + shareCount, + playCount, + trackUpdateTime, + name, + description, + commentCount, + trackIds, + createTime, + isFavorite, + ]; + + Map toJson() => _$PlaylistDetailToJson(this); + + PlaylistDetail copyWith({ + List? tracks, + User? creator, + String? coverUrl, + int? trackCount, + bool? subscribed, + int? subscribedCount, + int? shareCount, + int? playCount, + int? trackUpdateTime, + String? name, + String? description, + int? commentCount, + List? trackIds, + DateTime? createTime, + bool? isFavorite, + }) { + return PlaylistDetail( + id: id, + tracks: tracks ?? this.tracks, + creator: creator ?? this.creator, + coverUrl: coverUrl ?? this.coverUrl, + trackCount: trackCount ?? this.trackCount, + subscribed: subscribed ?? this.subscribed, + subscribedCount: subscribedCount ?? this.subscribedCount, + shareCount: shareCount ?? this.shareCount, + playCount: playCount ?? this.playCount, + trackUpdateTime: trackUpdateTime ?? this.trackUpdateTime, + name: name ?? this.name, + description: description ?? this.description, + commentCount: commentCount ?? this.commentCount, + trackIds: trackIds ?? this.trackIds, + createTime: createTime ?? this.createTime, + isFavorite: isFavorite ?? this.isFavorite, + ); + } +} diff --git a/lib/repository/data/playlist_detail.g.dart b/lib/repository/data/playlist_detail.g.dart new file mode 100644 index 0000000..4bf7354 --- /dev/null +++ b/lib/repository/data/playlist_detail.g.dart @@ -0,0 +1,132 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'playlist_detail.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PlaylistDetailAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + PlaylistDetail read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return PlaylistDetail( + id: fields[0] as int, + tracks: (fields[1] as List).cast(), + creator: fields[2] as User, + coverUrl: fields[3] as String, + trackCount: fields[4] as int, + subscribed: fields[5] as bool, + subscribedCount: fields[6] as int, + shareCount: fields[7] as int, + playCount: fields[8] as int, + trackUpdateTime: fields[9] as int, + name: fields[10] as String, + description: fields[11] as String, + commentCount: fields[12] as int, + trackIds: (fields[13] as List).cast(), + createTime: fields[14] as DateTime, + isFavorite: fields[15] as bool, + ); + } + + @override + void write(BinaryWriter writer, PlaylistDetail obj) { + writer + ..writeByte(16) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.tracks) + ..writeByte(2) + ..write(obj.creator) + ..writeByte(3) + ..write(obj.coverUrl) + ..writeByte(4) + ..write(obj.trackCount) + ..writeByte(5) + ..write(obj.subscribed) + ..writeByte(6) + ..write(obj.subscribedCount) + ..writeByte(7) + ..write(obj.shareCount) + ..writeByte(8) + ..write(obj.playCount) + ..writeByte(9) + ..write(obj.trackUpdateTime) + ..writeByte(10) + ..write(obj.name) + ..writeByte(11) + ..write(obj.description) + ..writeByte(12) + ..write(obj.commentCount) + ..writeByte(13) + ..write(obj.trackIds) + ..writeByte(14) + ..write(obj.createTime) + ..writeByte(15) + ..write(obj.isFavorite); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PlaylistDetailAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PlaylistDetail _$PlaylistDetailFromJson(Map json) => PlaylistDetail( + id: json['id'] as int, + tracks: (json['tracks'] as List) + .map((e) => Track.fromJson(Map.from(e as Map))) + .toList(), + creator: User.fromJson(Map.from(json['creator'] as Map)), + coverUrl: json['coverUrl'] as String, + trackCount: json['trackCount'] as int, + subscribed: json['subscribed'] as bool, + subscribedCount: json['subscribedCount'] as int, + shareCount: json['shareCount'] as int, + playCount: json['playCount'] as int, + trackUpdateTime: json['trackUpdateTime'] as int, + name: json['name'] as String, + description: json['description'] as String, + commentCount: json['commentCount'] as int, + trackIds: + (json['trackIds'] as List).map((e) => e as int).toList(), + createTime: DateTime.parse(json['createTime'] as String), + isFavorite: json['isFavorite'] as bool, + ); + +Map _$PlaylistDetailToJson(PlaylistDetail instance) => + { + 'id': instance.id, + 'tracks': instance.tracks.map((e) => e.toJson()).toList(), + 'creator': instance.creator.toJson(), + 'coverUrl': instance.coverUrl, + 'trackCount': instance.trackCount, + 'subscribed': instance.subscribed, + 'subscribedCount': instance.subscribedCount, + 'shareCount': instance.shareCount, + 'playCount': instance.playCount, + 'trackUpdateTime': instance.trackUpdateTime, + 'name': instance.name, + 'description': instance.description, + 'commentCount': instance.commentCount, + 'trackIds': instance.trackIds, + 'createTime': instance.createTime.toIso8601String(), + 'isFavorite': instance.isFavorite, + }; diff --git a/lib/repository/data/recommended_playlist.dart b/lib/repository/data/recommended_playlist.dart new file mode 100644 index 0000000..40b6ad8 --- /dev/null +++ b/lib/repository/data/recommended_playlist.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; + +class RecommendedPlaylist with EquatableMixin { + RecommendedPlaylist({ + required this.id, + required this.name, + required this.copywriter, + required this.picUrl, + required this.playCount, + required this.trackCount, + required this.alg, + }); + + final int id; + final String name; + final String copywriter; + + final String picUrl; + + final int playCount; + + final int trackCount; + + final String alg; + + @override + List get props => [ + id, + name, + copywriter, + picUrl, + playCount, + trackCount, + alg, + ]; +} diff --git a/lib/repository/data/search_result.dart b/lib/repository/data/search_result.dart new file mode 100644 index 0000000..6543007 --- /dev/null +++ b/lib/repository/data/search_result.dart @@ -0,0 +1,13 @@ +class SearchResult { + SearchResult({ + required this.result, + required this.hasMore, + required this.totalCount, + }); + + final T result; + + final bool hasMore; + + final int totalCount; +} diff --git a/lib/repository/data/track.dart b/lib/repository/data/track.dart new file mode 100644 index 0000000..3e484a1 --- /dev/null +++ b/lib/repository/data/track.dart @@ -0,0 +1,148 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'track.g.dart'; + +typedef Music = Track; + +@HiveType(typeId: 3) +enum TrackType { + @HiveField(0) + free, + @HiveField(1) + payAlbum, + @HiveField(2) + vip, + @HiveField(3) + cloud, + @HiveField(4) + noCopyright, +} + +@JsonSerializable() +@HiveType(typeId: 2) +class Track with EquatableMixin { + Track({ + required this.id, + required this.uri, + required this.name, + required this.artists, + required this.album, + required this.imageUrl, + required this.duration, + required this.type, + this.isRecommend = false, + }); + + factory Track.fromJson(Map json) => _$TrackFromJson(json); + + @HiveField(0) + final int id; + + @HiveField(1) + final String? uri; + + @HiveField(2) + final String name; + + @HiveField(3) + final List artists; + + @HiveField(4) + final AlbumMini? album; + + @HiveField(5) + final String? imageUrl; + + @HiveField(6) + final Duration duration; + + @HiveField(7) + final TrackType type; + + @HiveField(8, defaultValue: false) + final bool isRecommend; + + String get displaySubtitle { + final artist = artists.map((artist) => artist.name).join('/'); + return '$artist - ${album?.name ?? ''}'; + } + + String get artistString { + return artists.map((artist) => artist.name).join('/'); + } + + @override + List get props => [ + id, + uri, + name, + artists, + album, + imageUrl, + duration, + type, + isRecommend, + ]; + + Map toJson() => _$TrackToJson(this); +} + +@JsonSerializable() +@HiveType(typeId: 4) +class ArtistMini with EquatableMixin { + ArtistMini({ + required this.id, + required this.name, + required this.imageUrl, + }); + + factory ArtistMini.fromJson(Map json) => + _$ArtistMiniFromJson(json); + + @JsonKey(name: 'id') + @HiveField(0) + final int id; + @JsonKey(name: 'name') + @HiveField(1) + final String name; + @JsonKey(name: 'imageUrl') + @HiveField(2) + final String? imageUrl; + + @override + List get props => [id, name, imageUrl]; + + Map toJson() => _$ArtistMiniToJson(this); +} + +@JsonSerializable() +@HiveType(typeId: 5) +class AlbumMini with EquatableMixin { + AlbumMini({ + required this.id, + required this.picUri, + required this.name, + }); + + factory AlbumMini.fromJson(Map json) => + _$AlbumMiniFromJson(json); + + @JsonKey(name: 'id') + @HiveField(0) + final int id; + + @JsonKey(name: 'picUrl') + @HiveField(1) + final String? picUri; + + @JsonKey(name: 'name') + @HiveField(2) + final String name; + + @override + List get props => [id, picUri, name]; + + Map toJson() => _$AlbumMiniToJson(this); +} diff --git a/lib/repository/data/track.g.dart b/lib/repository/data/track.g.dart new file mode 100644 index 0000000..a303eed --- /dev/null +++ b/lib/repository/data/track.g.dart @@ -0,0 +1,264 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'track.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class TrackAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + Track read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Track( + id: fields[0] as int, + uri: fields[1] as String?, + name: fields[2] as String, + artists: (fields[3] as List).cast(), + album: fields[4] as AlbumMini?, + imageUrl: fields[5] as String?, + duration: fields[6] as Duration, + type: fields[7] as TrackType, + isRecommend: fields[8] == null ? false : fields[8] as bool, + ); + } + + @override + void write(BinaryWriter writer, Track obj) { + writer + ..writeByte(9) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.uri) + ..writeByte(2) + ..write(obj.name) + ..writeByte(3) + ..write(obj.artists) + ..writeByte(4) + ..write(obj.album) + ..writeByte(5) + ..write(obj.imageUrl) + ..writeByte(6) + ..write(obj.duration) + ..writeByte(7) + ..write(obj.type) + ..writeByte(8) + ..write(obj.isRecommend); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TrackAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ArtistMiniAdapter extends TypeAdapter { + @override + final int typeId = 4; + + @override + ArtistMini read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ArtistMini( + id: fields[0] as int, + name: fields[1] as String, + imageUrl: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, ArtistMini obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.imageUrl); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ArtistMiniAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class AlbumMiniAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + AlbumMini read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return AlbumMini( + id: fields[0] as int, + picUri: fields[1] as String?, + name: fields[2] as String, + ); + } + + @override + void write(BinaryWriter writer, AlbumMini obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.picUri) + ..writeByte(2) + ..write(obj.name); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AlbumMiniAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class TrackTypeAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + TrackType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return TrackType.free; + case 1: + return TrackType.payAlbum; + case 2: + return TrackType.vip; + case 3: + return TrackType.cloud; + case 4: + return TrackType.noCopyright; + default: + return TrackType.free; + } + } + + @override + void write(BinaryWriter writer, TrackType obj) { + switch (obj) { + case TrackType.free: + writer.writeByte(0); + break; + case TrackType.payAlbum: + writer.writeByte(1); + break; + case TrackType.vip: + writer.writeByte(2); + break; + case TrackType.cloud: + writer.writeByte(3); + break; + case TrackType.noCopyright: + writer.writeByte(4); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TrackTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Track _$TrackFromJson(Map json) => Track( + id: json['id'] as int, + uri: json['uri'] as String?, + name: json['name'] as String, + artists: (json['artists'] as List) + .map((e) => ArtistMini.fromJson(Map.from(e as Map))) + .toList(), + album: json['album'] == null + ? null + : AlbumMini.fromJson(Map.from(json['album'] as Map)), + imageUrl: json['imageUrl'] as String?, + duration: Duration(microseconds: json['duration'] as int), + type: $enumDecode(_$TrackTypeEnumMap, json['type']), + isRecommend: json['isRecommend'] as bool? ?? false, + ); + +Map _$TrackToJson(Track instance) => { + 'id': instance.id, + 'uri': instance.uri, + 'name': instance.name, + 'artists': instance.artists.map((e) => e.toJson()).toList(), + 'album': instance.album?.toJson(), + 'imageUrl': instance.imageUrl, + 'duration': instance.duration.inMicroseconds, + 'type': _$TrackTypeEnumMap[instance.type]!, + 'isRecommend': instance.isRecommend, + }; + +const _$TrackTypeEnumMap = { + TrackType.free: 'free', + TrackType.payAlbum: 'payAlbum', + TrackType.vip: 'vip', + TrackType.cloud: 'cloud', + TrackType.noCopyright: 'noCopyright', +}; + +ArtistMini _$ArtistMiniFromJson(Map json) => ArtistMini( + id: json['id'] as int, + name: json['name'] as String, + imageUrl: json['imageUrl'] as String?, + ); + +Map _$ArtistMiniToJson(ArtistMini instance) => + { + 'id': instance.id, + 'name': instance.name, + 'imageUrl': instance.imageUrl, + }; + +AlbumMini _$AlbumMiniFromJson(Map json) => AlbumMini( + id: json['id'] as int, + picUri: json['picUrl'] as String?, + name: json['name'] as String, + ); + +Map _$AlbumMiniToJson(AlbumMini instance) => { + 'id': instance.id, + 'picUrl': instance.picUri, + 'name': instance.name, + }; diff --git a/lib/repository/data/user.dart b/lib/repository/data/user.dart new file mode 100644 index 0000000..4208566 --- /dev/null +++ b/lib/repository/data/user.dart @@ -0,0 +1,100 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +@HiveType(typeId: 7) +class User with EquatableMixin { + User({ + required this.userId, + required this.avatarUrl, + required this.backgroundUrl, + required this.vipType, + required this.createTime, + required this.nickname, + required this.followed, + required this.description, + required this.detailDescription, + required this.followedUsers, + required this.followers, + required this.allSubscribedCount, + required this.playlistBeSubscribedCount, + required this.playlistCount, + required this.level, + required this.eventCount, + }); + + factory User.fromJson(Map json) => _$UserFromJson(json); + + @HiveField(0) + final int userId; + + @HiveField(1) + final String avatarUrl; + + @HiveField(2) + final String backgroundUrl; + + @HiveField(3) + final int vipType; + + @HiveField(4) + final int createTime; + + @HiveField(5) + final String nickname; + + @HiveField(6) + final bool followed; + + @HiveField(7) + final String description; + + @HiveField(8) + final String detailDescription; + + @HiveField(9) + final int followedUsers; + + @HiveField(10) + final int followers; + + @HiveField(11) + final int allSubscribedCount; + + @HiveField(12) + final int playlistBeSubscribedCount; + + @HiveField(13) + final int playlistCount; + + @HiveField(14) + final int eventCount; + + @HiveField(15) + final int level; + + @override + List get props => [ + userId, + avatarUrl, + backgroundUrl, + vipType, + createTime, + nickname, + followed, + description, + detailDescription, + followedUsers, + followers, + allSubscribedCount, + playlistBeSubscribedCount, + playlistCount, + level, + eventCount, + ]; + + Map toJson() => _$UserToJson(this); +} diff --git a/lib/repository/data/user.g.dart b/lib/repository/data/user.g.dart new file mode 100644 index 0000000..52275e7 --- /dev/null +++ b/lib/repository/data/user.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class UserAdapter extends TypeAdapter { + @override + final int typeId = 7; + + @override + User read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return User( + userId: fields[0] as int, + avatarUrl: fields[1] as String, + backgroundUrl: fields[2] as String, + vipType: fields[3] as int, + createTime: fields[4] as int, + nickname: fields[5] as String, + followed: fields[6] as bool, + description: fields[7] as String, + detailDescription: fields[8] as String, + followedUsers: fields[9] as int, + followers: fields[10] as int, + allSubscribedCount: fields[11] as int, + playlistBeSubscribedCount: fields[12] as int, + playlistCount: fields[13] as int, + level: fields[15] as int, + eventCount: fields[14] as int, + ); + } + + @override + void write(BinaryWriter writer, User obj) { + writer + ..writeByte(16) + ..writeByte(0) + ..write(obj.userId) + ..writeByte(1) + ..write(obj.avatarUrl) + ..writeByte(2) + ..write(obj.backgroundUrl) + ..writeByte(3) + ..write(obj.vipType) + ..writeByte(4) + ..write(obj.createTime) + ..writeByte(5) + ..write(obj.nickname) + ..writeByte(6) + ..write(obj.followed) + ..writeByte(7) + ..write(obj.description) + ..writeByte(8) + ..write(obj.detailDescription) + ..writeByte(9) + ..write(obj.followedUsers) + ..writeByte(10) + ..write(obj.followers) + ..writeByte(11) + ..write(obj.allSubscribedCount) + ..writeByte(12) + ..write(obj.playlistBeSubscribedCount) + ..writeByte(13) + ..write(obj.playlistCount) + ..writeByte(14) + ..write(obj.eventCount) + ..writeByte(15) + ..write(obj.level); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User( + userId: json['userId'] as int, + avatarUrl: json['avatarUrl'] as String, + backgroundUrl: json['backgroundUrl'] as String, + vipType: json['vipType'] as int, + createTime: json['createTime'] as int, + nickname: json['nickname'] as String, + followed: json['followed'] as bool, + description: json['description'] as String, + detailDescription: json['detailDescription'] as String, + followedUsers: json['followedUsers'] as int, + followers: json['followers'] as int, + allSubscribedCount: json['allSubscribedCount'] as int, + playlistBeSubscribedCount: json['playlistBeSubscribedCount'] as int, + playlistCount: json['playlistCount'] as int, + level: json['level'] as int, + eventCount: json['eventCount'] as int, + ); + +Map _$UserToJson(User instance) => { + 'userId': instance.userId, + 'avatarUrl': instance.avatarUrl, + 'backgroundUrl': instance.backgroundUrl, + 'vipType': instance.vipType, + 'createTime': instance.createTime, + 'nickname': instance.nickname, + 'followed': instance.followed, + 'description': instance.description, + 'detailDescription': instance.detailDescription, + 'followedUsers': instance.followedUsers, + 'followers': instance.followers, + 'allSubscribedCount': instance.allSubscribedCount, + 'playlistBeSubscribedCount': instance.playlistBeSubscribedCount, + 'playlistCount': instance.playlistCount, + 'eventCount': instance.eventCount, + 'level': instance.level, + }; diff --git a/lib/repository/local_cache_data.dart b/lib/repository/local_cache_data.dart new file mode 100644 index 0000000..664d32f --- /dev/null +++ b/lib/repository/local_cache_data.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:hive/hive.dart'; +import 'package:mixin_logger/mixin_logger.dart'; + +import '../utils/hive/hive_util.dart'; +import 'data/playlist_detail.dart'; +import 'data/track.dart'; + +LocalData neteaseLocalData = LocalData._(); + +const String _playHistoryKey = 'play_history'; + +class LocalData { + LocalData._(); + + final _box = Hive.openBoxSafe('local_data'); + + FutureOr operator [](dynamic key) async { + return get(key); + } + + void operator []=(dynamic key, dynamic value) { + _put(value, key); + } + + Future get(dynamic key) async { + final box = await _box; + try { + return box.get(key) as T?; + } catch (error, stackTrace) { + e('get $key error: $error\n$stackTrace'); + } + } + + Future _put(dynamic value, [dynamic key]) async { + final box = await _box; + try { + await box.put(key, value); + } catch (error, stacktrace) { + e('LocalData put error: $error\n$stacktrace'); + } + } + + Future getPlaylistDetail(int playlistId) async { + final data = await get>('playlist_detail_$playlistId'); + if (data == null) { + return null; + } + try { + return PlaylistDetail.fromJson(data); + } catch (e) { + return null; + } + } + + //TODO 添加分页加载逻辑 + Future updatePlaylistDetail(PlaylistDetail playlistDetail) { + return _put( + playlistDetail.toJson(), + 'playlist_detail_${playlistDetail.id}', + ); + } + + Future> getPlayHistory() async { + final data = await get>>(_playHistoryKey); + if (data == null) { + return const []; + } + final result = + data.cast>().map(Track.fromJson).toList(); + return result; + } + + Future updatePlayHistory(List list) { + return _put(list.map((t) => t.toJson()).toList(), _playHistoryKey); + } +} diff --git a/lib/repository/netease.dart b/lib/repository/netease.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/cache/cache.dart b/lib/utils/cache/cache.dart new file mode 100644 index 0000000..0bd8824 --- /dev/null +++ b/lib/utils/cache/cache.dart @@ -0,0 +1 @@ +export 'key_value_cache.dart'; diff --git a/lib/utils/cache/cached_image.dart b/lib/utils/cache/cached_image.dart new file mode 100644 index 0000000..b6f36b4 --- /dev/null +++ b/lib/utils/cache/cached_image.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui' as ui; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:path/path.dart' as p; + +import '../../repository/app_dir.dart'; +import 'key_value_cache.dart'; + +@immutable +class CachedImage extends ImageProvider implements CacheKey { + const CachedImage(this.url, {this.scale = 1.0, this.headers}); + + const CachedImage._internal( + this.url, { + this.scale = 1.0, + this.headers, + }); + + final String url; + + final double scale; + + final Map? headers; + + String get id => url.isEmpty ? '' : url.substring(url.lastIndexOf('/') + 1); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CachedImage && + runtimeType == other.runtimeType && + id == other.id && + scale == other.scale; + + @override + int get hashCode => Object.hash(id, scale); + + @override + ImageStreamCompleter loadBuffer( + CachedImage key, + DecoderBufferCallback decode, + ) { + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, decode), + scale: key.scale, + ); + } + + static final HttpClient _httpClient = HttpClient(); + + Future _loadAsync( + CachedImage key, + DecoderBufferCallback decode, + ) async { + final image = await ImageFileCache.instance.get(key); + if (image != null) { + return decode( + await ui.ImmutableBuffer.fromUint8List(image), + ); + } + + d('load image from network: $url $id'); + + if (key.url.isEmpty) { + throw Exception('image url is empty.'); + } + + //request network source + final resolved = Uri.base.resolve(key.url); + final request = await _httpClient.getUrl(resolved); + headers?.forEach((String name, String value) { + request.headers.add(name, value); + }); + final response = await request.close(); + if (response.statusCode != HttpStatus.ok) { + throw Exception( + 'HTTP request failed, statusCode: ${response.statusCode}, $resolved', + ); + } + + final bytes = await consolidateHttpClientResponseBytes(response); + if (bytes.lengthInBytes == 0) { + throw Exception('NetworkImage is an empty file: $resolved'); + } + + //save image to cache + await ImageFileCache.instance.update(key, bytes); + + return decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture( + CachedImage( + url, + scale: scale, + headers: headers, + ), + ); + } + + @override + String toString() { + return 'NeteaseImage{url: $url, scale: $scale}'; + } + + @override + String getKey() { + return id; + } +} + +class ImageFileCache implements Cache { + ImageFileCache._internal() + : provider = FileCacheProvider( + p.join(cacheDir.path, 'image'), + maxSize: 600 * 1024 * 1024 /* 600 Mb*/, + ); + + static final ImageFileCache instance = ImageFileCache._internal(); + + final FileCacheProvider provider; + + @override + Future get(CacheKey key) async { + final file = provider.getFile(key); + if (await file.exists()) { + provider.touchFile(file); + return Uint8List.fromList(await file.readAsBytes()); + } + return null; + } + + @override + Future update(CacheKey key, Uint8List? t) async { + var file = provider.getFile(key); + if (await file.exists()) { + await file.delete(); + } + file = await file.create(recursive: true); + await file.writeAsBytes(t!); + try { + return await file.exists(); + } finally { + provider.checkSize(); + } + } +} + +class IsolateImageCacheLoader { + IsolateImageCacheLoader(this.sendPort, this.imageUrl); + + final SendPort sendPort; + final String imageUrl; +} + +const _kImageCacheLoaderPortName = 'image_cache_loader_send_port'; + +final _receiverPort = ReceivePort('image_cache_provider'); + +void registerImageCacheProvider() { + if (!Platform.isAndroid && !Platform.isIOS) { + return; + } + _receiverPort.listen((message) async { + if (message is IsolateImageCacheLoader) { + d('IsolateImageCacheLoader: ${message.imageUrl} ${Isolate.current.debugName}'); + final image = ResizeImage( + CachedImage(message.imageUrl), + width: 200, + height: 200, + ); + final stream = image.resolve( + ImageConfiguration( + devicePixelRatio: window.devicePixelRatio, + locale: window.locale, + platform: defaultTargetPlatform, + ), + ); + final completer = Completer(); + ImageStreamListener? listener; + listener = ImageStreamListener( + (ImageInfo? image, bool sync) { + if (!completer.isCompleted) { + completer.complete( + image?.image + .toByteData(format: ImageByteFormat.png) + .then((value) => value?.buffer.asUint8List()), + ); + } + // Give callers until at least the end of the frame to subscribe to the + // image stream. + // See ImageCache._liveImages + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + stream.removeListener(listener!); + }); + }, + onError: (Object exception, StackTrace? stackTrace) { + if (!completer.isCompleted) { + completer.complete(null); + } + stream.removeListener(listener!); + e('failed to load image: $exception $stackTrace'); + }, + ); + stream.addListener(listener); + message.sendPort.send(await completer.future); + } + }); + ui.IsolateNameServer.removePortNameMapping(_kImageCacheLoaderPortName); + ui.IsolateNameServer.registerPortWithName( + _receiverPort.sendPort, + _kImageCacheLoaderPortName, + ); +} + +Future loadImageFromOtherIsolate(String? imageUrl) async { + if (imageUrl == null || imageUrl.isEmpty) { + return null; + } + final imageCachePort = ui.IsolateNameServer.lookupPortByName( + _kImageCacheLoaderPortName, + ); + if (imageCachePort == null) { + e('can not get imageCachePort in isolate: ${Isolate.current.debugName}'); + return null; + } + final receivePort = ReceivePort(); + imageCachePort.send(IsolateImageCacheLoader(receivePort.sendPort, imageUrl)); + final bytes = await receivePort.first; + receivePort.close(); + return bytes; +} diff --git a/lib/utils/cache/key_value_cache.dart b/lib/utils/cache/key_value_cache.dart new file mode 100644 index 0000000..472e638 --- /dev/null +++ b/lib/utils/cache/key_value_cache.dart @@ -0,0 +1,104 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:path/path.dart' as p; + +abstract class CacheKey { + factory CacheKey.fromString(String key) { + return _StringCacheKey(key); + } + + ///unique key to save or get a cache + String getKey(); +} + +class _StringCacheKey implements CacheKey { + const _StringCacheKey(this.key); + + final String key; + + @override + String getKey() { + return key; + } +} + +///base cache interface +///provide method to fetch or update cache object +abstract class Cache { + ///get cache object by key + ///null if no cache + Future get(CacheKey key); + + ///update cache by key + ///true if success + Future update(CacheKey key, T t); +} + +class FileCacheProvider { + FileCacheProvider(this.directory, {required this.maxSize}); + + final String directory; + + final int maxSize; + + bool _calculating = false; + + Future isCacheAvailable(CacheKey key) { + return _cacheFileForKey(key).exists(); + } + + File getFile(CacheKey key) { + return _cacheFileForKey(key); + } + + File _cacheFileForKey(CacheKey key) => File(p.join(directory, key.getKey())); + + void touchFile(File file) { + file.setLastModified(DateTime.now()).catchError((e) { + debugPrint('setLastModified for ${file.path} failed. $e'); + }); + } + + void checkSize() { + if (_calculating) { + return; + } + _calculating = true; + compute( + _fileLru, + {'path': directory, 'maxSize': maxSize}, + debugLabel: 'file lru check size', + ).whenComplete(() { + _calculating = false; + }); + } +} + +Future _fileLru(Map params) async { + final directory = Directory(params['path'] as String); + final maxSize = params['maxSize'] as int?; + if (!directory.existsSync()) { + return; + } + final files = directory.listSync().whereType().toList(); + files.sort((a, b) { + try { + return a.lastModifiedSync().compareTo(b.lastModifiedSync()); + } catch (error, stacktrace) { + e('_fileLru: error: $error, stacktrace: $stacktrace'); + return 0; + } + }); + + var totalSize = 0; + for (final file in files) { + if (totalSize > maxSize!) { + file.deleteSync(); + } else { + totalSize += file.lengthSync(); + } + } +} diff --git a/lib/utils/hive/duration_adapter.dart b/lib/utils/hive/duration_adapter.dart new file mode 100644 index 0000000..c030f30 --- /dev/null +++ b/lib/utils/hive/duration_adapter.dart @@ -0,0 +1,16 @@ +import 'package:hive/hive.dart'; + +class DurationAdapter extends TypeAdapter { + @override + final typeId = 6; + + @override + Duration read(BinaryReader reader) { + return Duration(milliseconds: reader.readInt()); + } + + @override + void write(BinaryWriter writer, Duration obj) { + writer.writeInt(obj.inMilliseconds); + } +} diff --git a/lib/utils/hive/hive_util.dart b/lib/utils/hive/hive_util.dart new file mode 100644 index 0000000..038c613 --- /dev/null +++ b/lib/utils/hive/hive_util.dart @@ -0,0 +1,15 @@ +import 'package:hive/hive.dart'; + +extension HiveExt on HiveInterface { + Future> openBoxSafe(String name) async { + try { + return await openBox( + name, + compactionStrategy: (entries, deleted) => false, + ); + } catch (error) { + await Hive.deleteBoxFromDisk(name); + return openBox(name); + } + } +} diff --git a/lib/utils/media_cache/cached_media_file.dart b/lib/utils/media_cache/cached_media_file.dart new file mode 100644 index 0000000..932bfbd --- /dev/null +++ b/lib/utils/media_cache/cached_media_file.dart @@ -0,0 +1,138 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:mixin_logger/mixin_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:synchronized/extension.dart'; + +const _downloadingSuffix = '.downloading'; + +const int _blockSize = 64 * 1024; + +class CachedMediaFile { + CachedMediaFile({ + required this.cacheFileName, + required this.url, + required this.cacheDir, + }) { + final file = File(p.join(cacheDir, cacheFileName)); + if (file.existsSync()) { + _file = file; + _completed = true; + } else { + _file = File(p.join(cacheDir, '$cacheFileName$_downloadingSuffix')); + _startDownload(_file); + } + } + + final String cacheDir; + + final String cacheFileName; + + final String url; + + var _completed = false; + + late File _file; + + final _responseContentLength = Completer(); + + Future get contentLength async { + if (_completed) { + return _file.length(); + } + return _responseContentLength.future; + } + + Future _startDownload(File tempFile) async { + final request = await HttpClient().getUrl(Uri.parse(url)); + if (tempFile.existsSync()) { + final length = tempFile.lengthSync(); + if (length > 0) { + request.headers.add('Range', 'bytes=$length-'); + } + } + final response = await request.close(); + + var startOffset = 0; + if (response.statusCode == HttpStatus.partialContent) { + final range = response.headers.value('content-range'); + if (range != null) { + final start = range.split(' ')[1].split('-')[0]; + startOffset = int.parse(start); + if (startOffset != tempFile.lengthSync()) { + throw Exception('startOffset != tempFile.lengthSync()'); + } + } + } else if (response.statusCode != HttpStatus.ok) { + if (response.statusCode == HttpStatus.requestedRangeNotSatisfiable) { + e('requestedRangeNotSatisfiable: $url . retry download'); + tempFile.deleteSync(); + unawaited(_startDownload(tempFile)); + return; + } + throw Exception('Failed to download file: $url ${response.statusCode}'); + } + + _responseContentLength.complete(response.contentLength + startOffset); + + final raf = await synchronized(() async { + if (startOffset == 0 && tempFile.existsSync()) { + await tempFile.delete(); + } + return tempFile.openSync(mode: FileMode.writeOnlyAppend); + }); + await for (final chunk in response) { + await synchronized(() async { + await raf.setPosition(raf.lengthSync()); + await raf.writeFrom(chunk); + }); + } + d('Download completed1 $url'); + await synchronized(() async { + await raf.close(); + final completedFile = File(p.join(cacheDir, cacheFileName)); + tempFile.renameSync(completedFile.path); + _file = completedFile; + _completed = true; + }); + d('Download completed $url'); + } + + Stream> stream(int start, int end) { + d('open stream ($start,$end) , $_completed ${_file.path}'); + if (_completed) { + return _file.openRead(start, end); + } + final controller = StreamController>(); + var readOffset = start; + Future readBlock() async => synchronized(() async { + if (_completed) { + await controller.addStream(_file.openRead(readOffset, end)); + await controller.close(); + return; + } + + final length = await _file.length(); + final raf = await _file.open(); + final readCount = math.min( + math.min(_blockSize, end - readOffset), + length - readOffset, + ); + await raf.setPosition(readOffset); + final buffer = await raf.read(readCount); + controller.add(List.from(buffer)); + readOffset += buffer.length; + await raf.close(); + if (readOffset < end) { + unawaited(readBlock()); + } else { + await controller.close(); + } + }); + + readBlock(); + return controller.stream; + } +} diff --git a/lib/utils/media_cache/media_cache.dart b/lib/utils/media_cache/media_cache.dart new file mode 100644 index 0000000..23b6880 --- /dev/null +++ b/lib/utils/media_cache/media_cache.dart @@ -0,0 +1,49 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../../repository/app_dir.dart'; +import 'media_cache_server.dart'; + +String _generateUniqueTrackCacheFileName(int id, String url) { + final uri = Uri.parse(url); + return '${id.hashCode}${p.extension(uri.path)}'; +} + +Future generateTrackProxyUrl(int id, String url) async { + final key = _generateUniqueTrackCacheFileName(id, url); + final cacheFile = await MediaCache.instance.getCached(key); + if (cacheFile != null) { + return Uri.file(cacheFile).toString(); + } + final proxyUrl = await MediaCache.instance.put(key, url); + return proxyUrl; +} + +class MediaCache { + MediaCache({required this.server}) { + cacheDir = Directory(p.join(appDir.path, 'media_cache')); + cacheDir.createSync(recursive: true); + } + + final MediaCacheServer server; + + static final MediaCache instance = MediaCache(server: MediaCacheServer()); + + late Directory cacheDir; + + Future getCached(String key) async { + final file = File(p.join(cacheDir.path, key)); + if (file.existsSync()) { + return file.path; + } + return null; + } + + Future put(String cacheFileName, String url) async { + await server.start(); + final proxyUrl = server.addProxyFile(cacheFileName, url, cacheDir.path); + return proxyUrl; + } +} diff --git a/lib/utils/media_cache/media_cache_server.dart b/lib/utils/media_cache/media_cache_server.dart new file mode 100644 index 0000000..67a4070 --- /dev/null +++ b/lib/utils/media_cache/media_cache_server.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:mixin_logger/mixin_logger.dart'; + +import '../pair.dart'; +import 'cached_media_file.dart'; + +const _kLocalProxyHost = 'localhost'; + +Pair _parsePartialRequestHeader(String value) { + final parts = value.split('='); + if (parts.length != 2) { + return Pair(0, null); + } + final range = parts[1]; + final rangeParts = range.split('-'); + if (rangeParts.length != 2) { + return Pair(0, null); + } + final start = int.tryParse(rangeParts[0]); + final end = int.tryParse(rangeParts[1]); + return Pair(start ?? 0, end); +} + +class MediaCacheServer { + MediaCacheServer(); + + StreamSubscription? _serverSubscription; + + final Map _cacheFiles = {}; + + bool get _isRunning => _serverSubscription != null; + + Future start() async { + if (_isRunning) { + return; + } + final server = await HttpServer.bind(_kLocalProxyHost, 10090, shared: true); + _serverSubscription = server.listen((request) { + _handleHttpRequest(request).catchError((error, stacktrace) { + e('MediaCacheServer: handle http request error $error $stacktrace'); + }); + }); + } + + Future _handleHttpRequest(HttpRequest request) async { + d('MediaCacheServer#_handleHttpRequest: ${request.uri.pathSegments}'); + final filename = request.uri.pathSegments.first; + final cacheFile = _cacheFiles[filename]; + if (cacheFile == null) { + request.response.statusCode = HttpStatus.notFound; + await request.response.close(); + return; + } + final contentLength = await cacheFile.contentLength; + request.response.headers.contentLength = contentLength; + request.response.headers.add(HttpHeaders.acceptRangesHeader, 'bytes'); + + // handle partial request + final range = request.headers.value(HttpHeaders.rangeHeader); + + var start = 0; + int? end; + if (range != null) { + final pair = _parsePartialRequestHeader(range); + start = pair.first; + end = pair.last; + } + if (start != 0 || end != null) { + request.response.statusCode = HttpStatus.partialContent; + request.response.headers.add( + HttpHeaders.contentRangeHeader, + 'bytes $start-${end ?? contentLength - 1}/$contentLength', + ); + request.response.headers.contentLength = + (end ?? contentLength - 1) - start + 1; + } + await request.response + .addStream(cacheFile.stream(start, (end ?? contentLength - 1) + 1)); + await request.response.close(); + } + + String addProxyFile(String filename, String url, String cacheDir) { + final proxyUrl = 'http://$_kLocalProxyHost:10090/$filename'; + if (!_cacheFiles.containsKey(filename)) { + final mediaCacheFile = CachedMediaFile( + cacheFileName: filename, + url: url, + cacheDir: cacheDir, + ); + _cacheFiles[filename] = mediaCacheFile; + } + return proxyUrl; + } + + Future stop() async { + await _serverSubscription?.cancel(); + } +} diff --git a/lib/utils/pair.dart b/lib/utils/pair.dart new file mode 100644 index 0000000..b1b5ee7 --- /dev/null +++ b/lib/utils/pair.dart @@ -0,0 +1,10 @@ +/// A pair of values. +class Pair { + Pair(this.first, this.last); + + final E first; + final F last; + + @override + String toString() => '($first, $last)'; +} diff --git a/lib/utils/platform_configuration.dart b/lib/utils/platform_configuration.dart new file mode 100644 index 0000000..3c6247d --- /dev/null +++ b/lib/utils/platform_configuration.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:window_manager/window_manager.dart'; + +const windowMinSize = Size(960, 720); + +class AppPlatformConfiguration extends HookWidget { + const AppPlatformConfiguration({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; + useEffect( + () { + if (Platform.isMacOS || Platform.isWindows) { + // update window min size when device pixel ratio changed. + // when move window from screen(4k) to screen(2k), window size will be changed. + // and window min size should be updated. + windowManager.setMinimumSize(windowMinSize); + } + }, + [devicePixelRatio], + ); + return child; + } +} diff --git a/lib/utils/system/system_fonts.dart b/lib/utils/system/system_fonts.dart new file mode 100644 index 0000000..949467f --- /dev/null +++ b/lib/utils/system/system_fonts.dart @@ -0,0 +1,182 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +bool _fallbackFontsLoaded = false; + +String? loadedFallbackFonts; + +// TODO(BIN): remove this when https://github.com/flutter/flutter/issues/90951 has been fixed. +Future loadFallbackFonts() async { + if (!Platform.isLinux) { + return; + } + + // Skip load fallback fonts if current system language is en. + // See more: https://github.com/flutter/flutter/issues/90951 + if (window.locale.languageCode == 'en') { + return; + } + if (_fallbackFontsLoaded) { + return; + } + _fallbackFontsLoaded = true; + + // On some Linux systems(Ubuntu 20.04), flutter can not render CJK fonts correctly when + // current system language is not en. + // https://github.com/flutter/flutter/issues/90951 + // We load the DroidSansFallbackFull font from the system and use it as a fallback. + try { + final matchedResult = Process.runSync('fc-match', ['-f', '%{family}']); + if (matchedResult.exitCode != 0) { + debugPrint( + 'failed to get best match font family. error: ${matchedResult.stderr}', + ); + return; + } + final result = Process.runSync( + 'fc-list', + ['-f', '%{family}:%{file}\n', matchedResult.stdout as String], + ); + final lines = const LineSplitter().convert(result.stdout as String); + String? fontFamily; + final fontPaths = []; + assert(lines.isNotEmpty); + for (final line in lines) { + // font config "family:file" + final fontConfig = line.split(':'); + assert( + fontConfig.length == 2, + 'font config do not match required format. $fontConfig', + ); + if (fontFamily == null) { + fontFamily = fontConfig.first; + fontPaths.add(fontConfig[1]); + } else if (fontFamily == fontConfig.first) { + fontPaths.add(fontConfig[1]); + } else { + debugPrint( + 'font family not match. expect $fontFamily, but ${fontConfig.first}. line: $line', + ); + } + } + if (fontPaths.isEmpty || fontFamily == null) { + debugPrint('failed to retriver font config: $lines'); + return; + } + loadedFallbackFonts = fontFamily; + for (final path in fontPaths) { + debugPrint('load fallback fonts: $fontFamily $path'); + try { + final file = File(path.trim()); + final bytes = file.readAsBytesSync(); + await loadFontFromList(bytes, fontFamily: fontFamily); + } catch (e, stacktrace) { + debugPrint('failed to load font $path, $e $stacktrace'); + } + } + } catch (error, stacktrace) { + debugPrint('failed to load system fonts, error: $error, $stacktrace'); + } +} + +extension ApplyFontsExtension on ThemeData { + ThemeData withFallbackFonts() { + if (loadedFallbackFonts == null) { + if (Platform.isWindows) { + return copyWith( + textTheme: textTheme.applyFonts(null, ['Microsoft Yahei']), + primaryTextTheme: + primaryTextTheme.applyFonts(null, ['Microsoft Yahei']), + ); + } else if (Platform.isIOS) { + return copyWith( + textTheme: + textTheme.applyFonts('PingFang SC', []).applyFontIosWeight(), + primaryTextTheme: primaryTextTheme + .applyFonts('PingFang SC', []).applyFontIosWeight(), + ); + } + return this; + } + return copyWith( + textTheme: textTheme.applyFonts(loadedFallbackFonts, null), + primaryTextTheme: primaryTextTheme.applyFonts(loadedFallbackFonts, null), + ); + } +} + +extension _TextTheme on TextTheme { + TextTheme applyFontIosWeight() => copyWith( + bodySmall: bodySmall?.copyWith(fontWeight: FontWeight.w400), + bodyMedium: bodyMedium?.copyWith(fontWeight: FontWeight.w500), + bodyLarge: bodyLarge?.copyWith(fontWeight: FontWeight.w400), + titleMedium: titleMedium?.copyWith(fontWeight: FontWeight.w500), + ); + + TextTheme applyFonts(String? fontFamily, List? fontFamilyFallback) => + copyWith( + displayLarge: displayLarge?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + displayMedium: displayMedium?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + displaySmall: displaySmall?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + headlineLarge: headlineLarge?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + headlineMedium: headlineMedium?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + headlineSmall: headlineSmall?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + titleLarge: titleLarge?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + titleMedium: titleMedium?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + titleSmall: titleSmall?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + bodyLarge: bodyLarge?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + bodyMedium: bodyMedium?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + bodySmall: bodySmall?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + labelLarge: labelLarge?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + labelMedium: labelMedium?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + labelSmall: labelSmall?.copyWith( + fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, + ), + ); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 25e690a..76c9a95 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,22 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2f5154f..7194bb7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + screen_retriever sentry_flutter url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 025750c..75fa731 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,17 +8,21 @@ import Foundation import package_info_plus import path_provider_foundation import photo_manager +import screen_retriever import sentry_flutter import shared_preferences_foundation import sqflite import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "PhotoManagerPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 539bff1..4a506e8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,272 +5,252 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "98d1d33ed129b372846e862de23a0fc365745f4d7b5e786ce667fcbbb7ac5c07" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "55.0.0" + version: "58.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "881348aed9b0b425882c97732629a6a31093c8ff20fc4b3b03fb9d3d50a3a126" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "5.7.1" + version: "5.10.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" archive: dependency: transitive description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.6" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.4.0" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.10.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" build: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.4.4" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" cli_util: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.3.5" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.4.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.17.0" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.2.5" + version: "2.3.0" dio: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.6" + equatable: + dependency: "direct main" + description: + name: equatable + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" extended_image: dependency: "direct main" description: name: extended_image - sha256: a6b738d9b8d5513be72c545cc3e9c5c451fbee77c8db3cbec7c32ae85b82fb93 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.4.1" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: b1de389378589e4dffb3564d782373238f19064037458092c49b3043b2791b2b - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.4.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" ffi: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" flutter: @@ -282,48 +262,56 @@ packages: dependency: transitive description: name: flutter_blurhash - sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + url: "https://pub.dartlang.org" + source: hosted + version: "0.18.6" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "0.11.0" + version: "0.12.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" flutter_staggered_grid_view: dependency: "direct main" description: name: flutter_staggered_grid_view - sha256: "1312314293acceb65b92754298754801b0e1f26a1845833b740b30415bbbcf07" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.2" flutter_swiper_null_safety: dependency: "direct main" description: name: flutter_swiper_null_safety - sha256: "5a855e0080d035c08e82f8b7fd2f106344943a30c9ab483b2584860a2f22eaaf" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" flutter_test: @@ -340,504 +328,499 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "2f9c4d3f4836421f7067a28f8939814597b27614e021da9d63e5d3fb6e212d25" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "8.2.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" getwidget: dependency: "direct main" description: name: getwidget - sha256: f98a1a96d946e640e4b5e3bd1fd692a50b53f3d1afc8c128f098139a0cf5607e - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" glob: dependency: transitive description: name: glob - sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.2.0" hexcolor: dependency: "direct main" description: name: hexcolor - sha256: c07f4bbb9095df87eeca87e7c69e8c3d60f70c66102d7b8d61c4af0453add3f6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" hgg_app_upgrade: dependency: "direct main" description: name: hgg_app_upgrade - sha256: f2e4d4b547779bb7e43318ce130e7f1623bbafd3bd1dffce5b8e2b4bbcfc96b6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + hive: + dependency: "direct main" + description: + name: hive + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" http: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_client_helper: dependency: transitive description: name: http_client_helper - sha256: "1f32359bd07a064ad256d1f84ae5f973f69bc972e7287223fa198abe1d969c28" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.0.2" image: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "4.0.15" image_gallery_saver: dependency: "direct main" description: name: image_gallery_saver - sha256: be812580c7a320d3bf583af89cac6b376f170d48000aca75215a73285a3223a0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.7.1" intl: dependency: transitive description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.18.0" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.4" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.8.0" json_model: dependency: "direct dev" description: name: json_model - sha256: c9a86ab62c800220cfe626fe64ab613a78bc39fc93c4ddf029b68e00b4d3fa87 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.6.1" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" logging: dependency: transitive description: name: logging - sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.12.13" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + mixin_logger: + dependency: "direct main" + description: + name: mixin_logger + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.3" modal_bottom_sheet: dependency: "direct main" description: name: modal_bottom_sheet - sha256: ef533916a2c3089571c32bd34e410faca77a6849a3f28f748e0794525c5658a0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + music_player: + dependency: "direct main" + description: + path: "." + ref: "434e6fdc79a81d1a2a0877339ae29004c0f32734" + resolved-ref: "434e6fdc79a81d1a2a0877339ae29004c0f32734" + url: "https://github.com/boyan01/flutter-music-player.git" + source: git + version: "0.0.1" nested: dependency: transitive description: name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + overlay_support: + dependency: "direct main" + description: + name: overlay_support + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.3" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" path_provider: dependency: "direct main" description: name: path_provider - sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.14" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.24" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "026b97a6c29da75181a37aae2eba9227f5fe13cb2838c6b975ce209328b8ab4e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.2.0" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.5" pedantic: dependency: transitive description: name: pedantic - sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "5.1.0" photo_manager: dependency: "direct main" description: name: photo_manager - sha256: "55d50ad1b8f984c57fa7c4bd4980f4760e80d3d9355263cf72624a6ff1bf2b5b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.5.2" + version: "2.6.0" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.4" pointycastle: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "3.6.2" + version: "3.7.2" pool: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.2" pull_to_refresh: dependency: "direct main" description: name: pull_to_refresh - sha256: bbadd5a931837b57739cf08736bea63167e284e71fb23b218c8c9a6e042aad12 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + riverpod: + dependency: transitive + description: + name: riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" rxdart: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.27.7" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" sentry: dependency: transitive description: name: sentry - sha256: a1529c545fcbc899e5dcc7c94ff1c6ad0c334dfc99a3cda366b1da98af7c5678 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.22.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: cab07e99a8f27af94f399cabceaff6968011660505b30a0e2286728a81bc476c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.22.0" settings_ui: dependency: "direct main" description: name: settings_ui - sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.18" + version: "2.0.20" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.17" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.5" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.5" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.3" sky_engine: @@ -849,240 +832,231 @@ packages: dependency: transitive description: name: source_gen - sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - sha256: "851d5040552cf911f4cabda08d003eca76b27da3ed0002978272e27c8fbf8ecc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.2.5" + version: "2.2.6" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "2.4.2+2" + version: "2.4.3" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.2+1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + system_clock: + dependency: transitive + description: + name: system_clock + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "0.4.16" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "845530e5e05db5500c1a4c1446785d60cbd8f9bd45e21e7dd643a3273bb4bbd1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "6.0.25" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7ab1e5b646623d6a2537aa59d5d039f90eebef75a7c25e105f6f75de1f7750c3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.5" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.1.4" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "2.3.0" win32: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.3" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "6.2.2" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" - url: "https://pub.flutter-io.cn" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" sdks: diff --git a/pubspec.yaml b/pubspec.yaml index 8c3b644..86ebcc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,6 +56,19 @@ dependencies: path_provider: ^2.0.12 sentry_flutter: ^6.20.1 photo_manager: ^2.5.2 + overlay_support: ^2.1.0 + flutter_riverpod: ^2.3.2 + equatable: ^2.0.5 + flutter_hooks: ^0.18.6 + window_manager: ^0.3.1 + hooks_riverpod: ^2.3.2 + mixin_logger: ^0.0.3 + hive: ^2.2.3 + music_player: + git: + url: https://github.com/boyan01/flutter-music-player.git + ref: 434e6fdc79a81d1a2a0877339ae29004c0f32734 +# lychee_player: ^0.4.0 dev_dependencies: flutter_test: @@ -69,8 +82,9 @@ dev_dependencies: flutter_lints: ^2.0.0 json_model: ^1.0.0 build_runner: ^2.3.3 - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.12.0 json_serializable: ^6.5.4 + hive_generator: ^2.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2272406..43d19d1 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,18 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 33f774d..6281416 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,11 +3,14 @@ # list(APPEND FLUTTER_PLUGIN_LIST + screen_retriever sentry_flutter url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + system_clock ) set(PLUGIN_BUNDLED_LIBRARIES)