From 719d9c8cc084d4608abdc1993a06614bdd1e5754 Mon Sep 17 00:00:00 2001 From: stonegate Date: Wed, 23 Sep 2020 22:19:07 +0800 Subject: [PATCH] Sync with gpodder.net. --- assets/gpodder.png | Bin 0 -> 35261 bytes lib/generated/intl/messages_pt.dart | 365 ++++++++ lib/generated/intl/messages_zh-Hans.dart | 94 +- lib/home/home_menu.dart | 11 +- lib/home/pocast_discovery.dart | 15 +- lib/home/search_podcast.dart | 10 +- lib/l10n/intl_en.arb | 46 + lib/l10n/intl_es.arb | 46 + lib/l10n/intl_fr.arb | 46 + lib/l10n/intl_pt.arb | 683 ++++++++++++++ lib/l10n/intl_zh_Hans.arb | 46 + lib/local_storage/key_value_storage.dart | 16 + lib/local_storage/sqflite_localpodcast.dart | 6 +- lib/podcasts/podcast_group.dart | 2 +- lib/podcasts/podcast_settings.dart | 2 +- lib/podcasts/podcastlist.dart | 2 +- lib/service/gpodder_api.dart | 239 +++++ .../{ompl_build.dart => opml_build.dart} | 9 +- .../{api_search.dart => search_api.dart} | 21 +- lib/settings/data_backup.dart | 836 +++++++++++++++--- lib/settings/history.dart | 4 +- lib/settings/settting.dart | 5 +- lib/settings/syncing.dart | 156 ++-- lib/settings/theme.dart | 6 +- lib/state/podcast_group.dart | 134 ++- lib/state/search_state.dart | 2 +- lib/state/setting_state.dart | 2 +- lib/type/search_api/itunes_podcast.dart | 67 ++ lib/type/search_api/itunes_podcast.g.dart | 41 + lib/type/{ => search_api}/search_genre.dart | 0 .../{ => search_api}/search_top_podcast.dart | 0 .../search_top_podcast.g.dart | 0 lib/type/{ => search_api}/searchepisodes.dart | 0 .../{ => search_api}/searchepisodes.g.dart | 0 lib/type/{ => search_api}/searchpodcast.dart | 0 .../{ => search_api}/searchpodcast.g.dart | 0 lib/util/custom_widget.dart | 7 +- lib/util/extension_helper.dart | 2 + lib/util/general_dialog.dart | 25 +- pubspec.yaml | 15 +- 40 files changed, 2651 insertions(+), 310 deletions(-) create mode 100644 assets/gpodder.png create mode 100644 lib/generated/intl/messages_pt.dart create mode 100644 lib/l10n/intl_pt.arb create mode 100644 lib/service/gpodder_api.dart rename lib/service/{ompl_build.dart => opml_build.dart} (93%) rename lib/service/{api_search.dart => search_api.dart} (71%) create mode 100644 lib/type/search_api/itunes_podcast.dart create mode 100644 lib/type/search_api/itunes_podcast.g.dart rename lib/type/{ => search_api}/search_genre.dart (100%) rename lib/type/{ => search_api}/search_top_podcast.dart (100%) rename lib/type/{ => search_api}/search_top_podcast.g.dart (100%) rename lib/type/{ => search_api}/searchepisodes.dart (100%) rename lib/type/{ => search_api}/searchepisodes.g.dart (100%) rename lib/type/{ => search_api}/searchpodcast.dart (100%) rename lib/type/{ => search_api}/searchpodcast.g.dart (100%) diff --git a/assets/gpodder.png b/assets/gpodder.png new file mode 100644 index 0000000000000000000000000000000000000000..d68d1f9aa19463f9666f1858ff0f1ab0218ac328 GIT binary patch literal 35261 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 args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'pt'; + + static m0(groupName, count) => "${Intl.plural(count, zero: '', one: '${count} episódio de ${groupName} adicionado à lista', other: '${count} episódios de ${groupName} adicionados à lista')}"; + + static m1(count) => "${Intl.plural(count, zero: '', one: '${count} episódio adicionado à lista', other: '${count} episódios adicionados à lista')}"; + + static m2(count) => "${Intl.plural(count, zero: 'Hoje', one: 'Há ${count} dia', other: 'Há ${count} dias')}"; + + static m3(count) => "${Intl.plural(count, zero: 'Nunca', one: '${count} dia', other: '${count} dias')}"; + + static m4(count) => "${Intl.plural(count, zero: '', one: 'Episódio', other: 'Episódios')}"; + + static m5(time) => "De ${time}"; + + static m6(count) => "${Intl.plural(count, zero: 'Grupo', one: 'Grupo', other: 'Grupos')}"; + + static m7(host) => "Hospedado em ${host}"; + + static m8(count) => "${Intl.plural(count, zero: '', one: 'há ${count} hora', other: 'há ${count} horas')}"; + + static m9(count) => "${Intl.plural(count, zero: '0 horas', one: '${count} hora', other: '${count} horas')}"; + + static m10(service) => "Integrate with ${service}"; + + static m11(userName) => "Logged in as ${userName}"; + + static m12(count) => "${Intl.plural(count, zero: 'Agora', one: 'Há ${count} minuto', other: 'Há ${count} minutos')}"; + + static m13(count) => "${Intl.plural(count, zero: '0 minutos', one: '${count} minuto', other: '${count} minutos')}"; + + static m14(title) => "Obter dados ${title}"; + + static m15(title) => "A subscrição falhou, erro de rede ${title}"; + + static m16(title) => "Subscrever ${title}"; + + static m17(title) => "Subscrição falhou, podcast já existe ${title}"; + + static m18(title) => "Subscrito com sucesso ${title}"; + + static m19(title) => "Atualizar ${title}"; + + static m20(title) => "Erro de atualização ${title}"; + + static m21(count) => "${Intl.plural(count, zero: '', one: 'Podcast', other: 'Podcasts')}"; + + static m22(date) => "Publicado em ${date}"; + + static m23(date) => "Removido em ${date}"; + + static m24(count) => "${Intl.plural(count, zero: '0 segundos', one: '${count} segundo', other: '${count} segundos')}"; + + static m25(count) => "${Intl.plural(count, zero: 'Agora', one: 'Há ${count} segundo', other: 'Há ${count} segundos')}"; + + static m26(time) => "Última vez ${time}"; + + static m27(time) => "${time} Restante"; + + static m28(time) => "Para ${time}"; + + static m29(count) => "${Intl.plural(count, zero: 'Sem atualizações', one: '${count} episódio atualizado', other: '${count} episódios atualizados')}"; + + static m30(version) => "Versão: ${version}"; + + final messages = _notInlinedMessages(_notInlinedMessages); + static _notInlinedMessages(_) => { + "add" : MessageLookupByLibrary.simpleMessage("Adicionar"), + "addEpisodeGroup" : m0, + "addNewEpisodeAll" : m1, + "addNewEpisodeTooltip" : MessageLookupByLibrary.simpleMessage("Adiciona novos episódios à lista de reprodução"), + "addSomeGroups" : MessageLookupByLibrary.simpleMessage("Adiciona alguns grupos"), + "all" : MessageLookupByLibrary.simpleMessage("Todos"), + "autoDownload" : MessageLookupByLibrary.simpleMessage("Download automático"), + "back" : MessageLookupByLibrary.simpleMessage("Atrás"), + "boostVolume" : MessageLookupByLibrary.simpleMessage("Aumentar volume"), + "buffering" : MessageLookupByLibrary.simpleMessage("A carregar"), + "cancel" : MessageLookupByLibrary.simpleMessage("CANCELAR"), + "cellularConfirm" : MessageLookupByLibrary.simpleMessage("Alerta de dados móveis"), + "cellularConfirmDes" : MessageLookupByLibrary.simpleMessage("Tens a certeza que queres usar dados móveis para downloads?"), + "changeLayout" : MessageLookupByLibrary.simpleMessage("Mudar aparência"), + "changelog" : MessageLookupByLibrary.simpleMessage("Registo de mudanças"), + "chooseA" : MessageLookupByLibrary.simpleMessage("Escolher um"), + "clear" : MessageLookupByLibrary.simpleMessage("Limpar"), + "color" : MessageLookupByLibrary.simpleMessage("Cor"), + "confirm" : MessageLookupByLibrary.simpleMessage("CONFIRMAR"), + "darkMode" : MessageLookupByLibrary.simpleMessage("Modo escuro"), + "daysAgo" : m2, + "daysCount" : m3, + "delete" : MessageLookupByLibrary.simpleMessage("Eliminar"), + "developer" : MessageLookupByLibrary.simpleMessage("Desenvolvedor"), + "dismiss" : MessageLookupByLibrary.simpleMessage("Minimizar"), + "done" : MessageLookupByLibrary.simpleMessage("Feito"), + "download" : MessageLookupByLibrary.simpleMessage("Download"), + "downloadRemovedToast" : MessageLookupByLibrary.simpleMessage("Download removido"), + "downloaded" : MessageLookupByLibrary.simpleMessage("Descarregado"), + "editGroupName" : MessageLookupByLibrary.simpleMessage("Editar nome do grupo"), + "endOfEpisode" : MessageLookupByLibrary.simpleMessage("Fim do episódio"), + "episode" : m4, + "fastForward" : MessageLookupByLibrary.simpleMessage("Avanço"), + "fastRewind" : MessageLookupByLibrary.simpleMessage("Recuo rápido"), + "featureDiscoveryEditGroup" : MessageLookupByLibrary.simpleMessage("Prime para editar grupo"), + "featureDiscoveryEditGroupDes" : MessageLookupByLibrary.simpleMessage("Podes alterar o nome do grupo ou apagá-lo aqui, mas o grupo Home não pode ser editado ou eliminado"), + "featureDiscoveryEpisode" : MessageLookupByLibrary.simpleMessage("Vista de episódios"), + "featureDiscoveryEpisodeDes" : MessageLookupByLibrary.simpleMessage("Podes manter premido para reproduzir um episódio ou adicioná-lo a uma lista de reprodução."), + "featureDiscoveryEpisodeTitle" : MessageLookupByLibrary.simpleMessage("Mantém premido para reproduzir um episódio instantâneamente"), + "featureDiscoveryGroup" : MessageLookupByLibrary.simpleMessage("Prime para adicionar grupo"), + "featureDiscoveryGroupDes" : MessageLookupByLibrary.simpleMessage("O grupo por defeito para novos podcasts é Home. Podes criar novos grupos e mover os podcasts para estes, assim como adicionar podcasts a múltiplos grupos."), + "featureDiscoveryGroupPodcast" : MessageLookupByLibrary.simpleMessage("Mantém premido para reordenar podcasts"), + "featureDiscoveryGroupPodcastDes" : MessageLookupByLibrary.simpleMessage("Podes premir para ver mais opções, ou manter premido para reordenar podcasts em grupos."), + "featureDiscoveryOMPL" : MessageLookupByLibrary.simpleMessage("Premir para importar um OPML"), + "featureDiscoveryOMPLDes" : MessageLookupByLibrary.simpleMessage("Podes importar ficheiros OPML, abrir as definições ou atualizar todos os podcasts aqui."), + "featureDiscoveryPlaylist" : MessageLookupByLibrary.simpleMessage("Prime para abrir a lista de reprodução"), + "featureDiscoveryPlaylistDes" : MessageLookupByLibrary.simpleMessage("Podes adicionar episódios à lista de reprodução manualmente. Os episódios serão automaticamente removidos das listas de reprodução quando reproduzidos."), + "featureDiscoveryPodcast" : MessageLookupByLibrary.simpleMessage("Vista do podcast"), + "featureDiscoveryPodcastDes" : MessageLookupByLibrary.simpleMessage("Podes premir \"Ver Todos\" para adicionar grupos ou organizar pdcasts."), + "featureDiscoveryPodcastTitle" : MessageLookupByLibrary.simpleMessage("Deslizar verticalmente para alterar grupos"), + "featureDiscoverySearch" : MessageLookupByLibrary.simpleMessage("Prime para procurar podcasts"), + "featureDiscoverySearchDes" : MessageLookupByLibrary.simpleMessage("Podes procurar pelo título do podcast, palavra-chave ou ligação RSS para subscrever novos podcasts."), + "feedbackEmail" : MessageLookupByLibrary.simpleMessage("Escreve-me"), + "feedbackGithub" : MessageLookupByLibrary.simpleMessage("Submeter problema"), + "feedbackPlay" : MessageLookupByLibrary.simpleMessage("Avaliar na Play Store"), + "feedbackTelegram" : MessageLookupByLibrary.simpleMessage("Juntar um grupo"), + "filter" : MessageLookupByLibrary.simpleMessage("Filtro"), + "fontStyle" : MessageLookupByLibrary.simpleMessage("Estilo do tipo de letra"), + "fonts" : MessageLookupByLibrary.simpleMessage("Fontes"), + "from" : m5, + "goodNight" : MessageLookupByLibrary.simpleMessage("Boa Noite"), + "gpodderLoginDes" : MessageLookupByLibrary.simpleMessage("Congratulations! You have linked gpodder.net account successfully. Tsacdop will automatically sync subscriptions on your device with your gpodder.net account."), + "groupExisted" : MessageLookupByLibrary.simpleMessage("Grupo já existe"), + "groupFilter" : MessageLookupByLibrary.simpleMessage("Filtro de grupo"), + "groupRemoveConfirm" : MessageLookupByLibrary.simpleMessage("Tens a certeza que queres eliminar este grupo? Os podcasts serão removidos para o grupo \"Home\"."), + "groups" : m6, + "hideListenedSetting" : MessageLookupByLibrary.simpleMessage("Esconder ouvidos"), + "homeGroupsSeeAll" : MessageLookupByLibrary.simpleMessage("Ver Todos"), + "homeMenuPlaylist" : MessageLookupByLibrary.simpleMessage("Lista de Reprodução"), + "homeSubMenuSortBy" : MessageLookupByLibrary.simpleMessage("Ordenar por"), + "homeTabMenuFavotite" : MessageLookupByLibrary.simpleMessage("Favorito"), + "homeTabMenuRecent" : MessageLookupByLibrary.simpleMessage("Recentes"), + "homeToprightMenuAbout" : MessageLookupByLibrary.simpleMessage("Sobre"), + "homeToprightMenuImportOMPL" : MessageLookupByLibrary.simpleMessage("Importar OPML"), + "homeToprightMenuRefreshAll" : MessageLookupByLibrary.simpleMessage("Atualizar todos"), + "hostedOn" : m7, + "hoursAgo" : m8, + "hoursCount" : m9, + "import" : MessageLookupByLibrary.simpleMessage("Importar"), + "intergateWith" : m10, + "introFourthPage" : MessageLookupByLibrary.simpleMessage("Podes manter premido um episódio para uma ação rápida."), + "introSecondPage" : MessageLookupByLibrary.simpleMessage("Subscreve podcasts por pesquisa ou importa um ficheiro OPML."), + "introThirdPage" : MessageLookupByLibrary.simpleMessage("Podes criar um novo grupo para podcasts."), + "invalidName" : MessageLookupByLibrary.simpleMessage("Invalid username"), + "lastUpdate" : MessageLookupByLibrary.simpleMessage("Last update"), + "later" : MessageLookupByLibrary.simpleMessage("Mais tarde"), + "lightMode" : MessageLookupByLibrary.simpleMessage("Modo claro"), + "like" : MessageLookupByLibrary.simpleMessage("Gosto"), + "likeDate" : MessageLookupByLibrary.simpleMessage("Data do Gosto"), + "liked" : MessageLookupByLibrary.simpleMessage("Gostou"), + "listen" : MessageLookupByLibrary.simpleMessage("Ouvir"), + "listened" : MessageLookupByLibrary.simpleMessage("Ouvido"), + "loadMore" : MessageLookupByLibrary.simpleMessage("Carregar mais"), + "loggedInAs" : m11, + "login" : MessageLookupByLibrary.simpleMessage("Login"), + "loginFailed" : MessageLookupByLibrary.simpleMessage("Login failed"), + "logout" : MessageLookupByLibrary.simpleMessage("Logout"), + "mark" : MessageLookupByLibrary.simpleMessage("Marcar"), + "markConfirm" : MessageLookupByLibrary.simpleMessage("Confirmar marca"), + "markConfirmContent" : MessageLookupByLibrary.simpleMessage("Marcar todos os episódios como ouvidos?"), + "markListened" : MessageLookupByLibrary.simpleMessage("Marcar como ouvido"), + "markNotListened" : MessageLookupByLibrary.simpleMessage("Marcar não ouvidos"), + "menu" : MessageLookupByLibrary.simpleMessage("Menu"), + "menuAllPodcasts" : MessageLookupByLibrary.simpleMessage("Todos os podcasts"), + "menuMarkAllListened" : MessageLookupByLibrary.simpleMessage("Marcar todos como ouvidos"), + "menuViewRSS" : MessageLookupByLibrary.simpleMessage("Visitar Feed RSS"), + "menuVisitSite" : MessageLookupByLibrary.simpleMessage("Visitar website"), + "minsAgo" : m12, + "minsCount" : m13, + "network" : MessageLookupByLibrary.simpleMessage("Rede"), + "newGroup" : MessageLookupByLibrary.simpleMessage("Criar um novo grupo"), + "newestFirst" : MessageLookupByLibrary.simpleMessage("Mais recentes primeiro"), + "next" : MessageLookupByLibrary.simpleMessage("Seguinte"), + "noEpisodeDownload" : MessageLookupByLibrary.simpleMessage("Ainda não há episódios descarregados"), + "noEpisodeFavorite" : MessageLookupByLibrary.simpleMessage("Ainda não há episódios coletados"), + "noEpisodeRecent" : MessageLookupByLibrary.simpleMessage("Ainda não há episódios recebidos"), + "noPodcastGroup" : MessageLookupByLibrary.simpleMessage("Não há podcasts neste grupo"), + "noShownote" : MessageLookupByLibrary.simpleMessage("Não há notas disponíveis para este episódio"), + "notificaitonFatch" : m14, + "notificationNetworkError" : m15, + "notificationSetting" : MessageLookupByLibrary.simpleMessage("Painel de notificações"), + "notificationSubscribe" : m16, + "notificationSubscribeExisted" : m17, + "notificationSuccess" : m18, + "notificationUpdate" : m19, + "notificationUpdateError" : m20, + "oldestFirst" : MessageLookupByLibrary.simpleMessage("Mais antigos primeiro"), + "passwdRequired" : MessageLookupByLibrary.simpleMessage("Password required"), + "password" : MessageLookupByLibrary.simpleMessage("Password"), + "pause" : MessageLookupByLibrary.simpleMessage("Pausa"), + "play" : MessageLookupByLibrary.simpleMessage("Reproduzir"), + "playback" : MessageLookupByLibrary.simpleMessage("Controlo da reprodução"), + "player" : MessageLookupByLibrary.simpleMessage("Reprodutor"), + "playerHeightMed" : MessageLookupByLibrary.simpleMessage("Médio"), + "playerHeightShort" : MessageLookupByLibrary.simpleMessage("Baixo"), + "playerHeightTall" : MessageLookupByLibrary.simpleMessage("Alto"), + "playing" : MessageLookupByLibrary.simpleMessage("Em reprodução"), + "plugins" : MessageLookupByLibrary.simpleMessage("Plugins"), + "podcast" : m21, + "podcastSubscribed" : MessageLookupByLibrary.simpleMessage("Podcast subscrito"), + "popupMenuDownloadDes" : MessageLookupByLibrary.simpleMessage("Descarregar episódio"), + "popupMenuLaterDes" : MessageLookupByLibrary.simpleMessage("Adicionar episódio à lista de reprodução"), + "popupMenuLikeDes" : MessageLookupByLibrary.simpleMessage("Adicionar episódio aos favoritos"), + "popupMenuMarkDes" : MessageLookupByLibrary.simpleMessage("Marcar episódio como ouvido"), + "popupMenuPlayDes" : MessageLookupByLibrary.simpleMessage("Reproduzir episódio"), + "privacyPolicy" : MessageLookupByLibrary.simpleMessage("Política de Privacidade"), + "published" : m22, + "publishedDaily" : MessageLookupByLibrary.simpleMessage("Publicado diariamente"), + "publishedMonthly" : MessageLookupByLibrary.simpleMessage("Publicado mensalmente"), + "publishedWeekly" : MessageLookupByLibrary.simpleMessage("Publicado semanalmente"), + "publishedYearly" : MessageLookupByLibrary.simpleMessage("Publicado anualmente"), + "recoverSubscribe" : MessageLookupByLibrary.simpleMessage("Recuperar subscrição"), + "refreshArtwork" : MessageLookupByLibrary.simpleMessage("Atualizar capa"), + "remove" : MessageLookupByLibrary.simpleMessage("Remover"), + "removeConfirm" : MessageLookupByLibrary.simpleMessage("Confirmação de remoção"), + "removePodcastDes" : MessageLookupByLibrary.simpleMessage("Tens a certeza que pretendes cancelar a subscrição?"), + "removedAt" : m23, + "save" : MessageLookupByLibrary.simpleMessage("Guardar"), + "schedule" : MessageLookupByLibrary.simpleMessage("Horário"), + "search" : MessageLookupByLibrary.simpleMessage("Procurar"), + "searchEpisode" : MessageLookupByLibrary.simpleMessage("Procurar episódio"), + "searchInvalidRss" : MessageLookupByLibrary.simpleMessage("Ligação RSS inválida"), + "searchPodcast" : MessageLookupByLibrary.simpleMessage("Procurar podcasts"), + "secCount" : m24, + "secondsAgo" : m25, + "settingStorage" : MessageLookupByLibrary.simpleMessage("Armazenamento"), + "settings" : MessageLookupByLibrary.simpleMessage("Definições"), + "settingsAccentColor" : MessageLookupByLibrary.simpleMessage("Cor de realce"), + "settingsAccentColorDes" : MessageLookupByLibrary.simpleMessage("Incluir cor de sobreposição"), + "settingsAppIntro" : MessageLookupByLibrary.simpleMessage("Introdução da Aplicação"), + "settingsAppearance" : MessageLookupByLibrary.simpleMessage("Aparência"), + "settingsAppearanceDes" : MessageLookupByLibrary.simpleMessage("Cores e temas"), + "settingsAudioCache" : MessageLookupByLibrary.simpleMessage("Cache de áudio"), + "settingsAudioCacheDes" : MessageLookupByLibrary.simpleMessage("Tamanho máximo da cache de áudio"), + "settingsAutoDelete" : MessageLookupByLibrary.simpleMessage("Eliminar downloads automaticamente após"), + "settingsAutoDeleteDes" : MessageLookupByLibrary.simpleMessage("30 dias por defeito"), + "settingsAutoPlayDes" : MessageLookupByLibrary.simpleMessage("Reproduzir automaticamente o episódio seguinte"), + "settingsBackup" : MessageLookupByLibrary.simpleMessage("Cópia de segurança"), + "settingsBackupDes" : MessageLookupByLibrary.simpleMessage("Cópia de segurança dos dados da aplicação"), + "settingsBoostVolume" : MessageLookupByLibrary.simpleMessage("Nível de aumento de volume"), + "settingsBoostVolumeDes" : MessageLookupByLibrary.simpleMessage("Alterar nível de aumento de volume"), + "settingsDefaultGrid" : MessageLookupByLibrary.simpleMessage("Vista de grelha predefinida"), + "settingsDefaultGridDownload" : MessageLookupByLibrary.simpleMessage("Aba de downloads"), + "settingsDefaultGridFavorite" : MessageLookupByLibrary.simpleMessage("Aba de favoritos"), + "settingsDefaultGridPodcast" : MessageLookupByLibrary.simpleMessage("Página de podcasts"), + "settingsDefaultGridRecent" : MessageLookupByLibrary.simpleMessage("Aba de recentes"), + "settingsDiscovery" : MessageLookupByLibrary.simpleMessage("Reiniciar tutorial"), + "settingsEnableSyncing" : MessageLookupByLibrary.simpleMessage("Ativar sincronização"), + "settingsEnableSyncingDes" : MessageLookupByLibrary.simpleMessage("Atualizar todos os podcasts em segundo plano para obter os episódios mais recentes"), + "settingsExportDes" : MessageLookupByLibrary.simpleMessage("Exportar e importar definições da aplicação"), + "settingsFastForwardSec" : MessageLookupByLibrary.simpleMessage("Avançar segundos"), + "settingsFastForwardSecDes" : MessageLookupByLibrary.simpleMessage("Muda os segundos de avanço no reprodutor"), + "settingsFeedback" : MessageLookupByLibrary.simpleMessage("Feedback"), + "settingsFeedbackDes" : MessageLookupByLibrary.simpleMessage("Erros e sugestões"), + "settingsHistory" : MessageLookupByLibrary.simpleMessage("Histórico"), + "settingsHistoryDes" : MessageLookupByLibrary.simpleMessage("Dados de audição"), + "settingsInfo" : MessageLookupByLibrary.simpleMessage("Informações"), + "settingsInterface" : MessageLookupByLibrary.simpleMessage("Interface"), + "settingsLanguages" : MessageLookupByLibrary.simpleMessage("Idiomas"), + "settingsLanguagesDes" : MessageLookupByLibrary.simpleMessage("Mudar idioma"), + "settingsLayout" : MessageLookupByLibrary.simpleMessage("Esquema"), + "settingsLayoutDes" : MessageLookupByLibrary.simpleMessage("Esquema da aplicação"), + "settingsLibraries" : MessageLookupByLibrary.simpleMessage("Bibliotecas"), + "settingsLibrariesDes" : MessageLookupByLibrary.simpleMessage("Bibliotecas de código aberto usados nesta aplicação"), + "settingsManageDownload" : MessageLookupByLibrary.simpleMessage("Gerir downloads"), + "settingsManageDownloadDes" : MessageLookupByLibrary.simpleMessage("Gerir arquivos de aúdio descarregados"), + "settingsMenuAutoPlay" : MessageLookupByLibrary.simpleMessage("Reproduzir seguinte automaticamente"), + "settingsNetworkCellular" : MessageLookupByLibrary.simpleMessage("Perguntar antes de usar dados móveis"), + "settingsNetworkCellularAuto" : MessageLookupByLibrary.simpleMessage("Descarregar automaticamente usando os dados móveis"), + "settingsNetworkCellularAutoDes" : MessageLookupByLibrary.simpleMessage("Podes configurar o descarregamento automático na página de gestão de grupos"), + "settingsNetworkCellularDes" : MessageLookupByLibrary.simpleMessage("Perguntar a confirmar o uso de dados móveis ao descarregar episódios"), + "settingsPlayDes" : MessageLookupByLibrary.simpleMessage("Lista de reprodução e reprodutor"), + "settingsPlayerHeight" : MessageLookupByLibrary.simpleMessage("Altura do reprodutor"), + "settingsPlayerHeightDes" : MessageLookupByLibrary.simpleMessage("Mudar a altura do reprodutor a teu gosto"), + "settingsPopupMenu" : MessageLookupByLibrary.simpleMessage("Menu pop-up de episódios"), + "settingsPopupMenuDes" : MessageLookupByLibrary.simpleMessage("Muda o menu pop-up de episódios"), + "settingsPrefrence" : MessageLookupByLibrary.simpleMessage("Preferências"), + "settingsRealDark" : MessageLookupByLibrary.simpleMessage("Escuro AMOLED"), + "settingsRealDarkDes" : MessageLookupByLibrary.simpleMessage("Ativa caso o modo escuro não seja suficientemente escuro"), + "settingsRewindSec" : MessageLookupByLibrary.simpleMessage("Segundos de recuo"), + "settingsRewindSecDes" : MessageLookupByLibrary.simpleMessage("Muda os segundos de recuo no reprodutor"), + "settingsSTAuto" : MessageLookupByLibrary.simpleMessage("Ligar temporizador automaticamente"), + "settingsSTAutoDes" : MessageLookupByLibrary.simpleMessage("Ligar temporizador automaticamente num horário definido"), + "settingsSTDefaultTime" : MessageLookupByLibrary.simpleMessage("Tempo predefinido"), + "settingsSTDefautTimeDes" : MessageLookupByLibrary.simpleMessage("Tempo predefinido para temporizador"), + "settingsSTMode" : MessageLookupByLibrary.simpleMessage("Modo de temporizador automático"), + "settingsSpeeds" : MessageLookupByLibrary.simpleMessage("Velocidades"), + "settingsSpeedsDes" : MessageLookupByLibrary.simpleMessage("Customizar as velocidades disponíveis"), + "settingsStorageDes" : MessageLookupByLibrary.simpleMessage("Gerir cache e armazenamento de downloads"), + "settingsSyncing" : MessageLookupByLibrary.simpleMessage("Sincronização"), + "settingsSyncingDes" : MessageLookupByLibrary.simpleMessage("Atualizar podcasts em segundo plano"), + "settingsTapToOpenPopupMenu" : MessageLookupByLibrary.simpleMessage("Prime para abrir o menu pop-up"), + "settingsTapToOpenPopupMenuDes" : MessageLookupByLibrary.simpleMessage("Precisas manter premido para abrir a página do episódio"), + "settingsTheme" : MessageLookupByLibrary.simpleMessage("Tema"), + "settingsUpdateInterval" : MessageLookupByLibrary.simpleMessage("Intervalo de atualização"), + "settingsUpdateIntervalDes" : MessageLookupByLibrary.simpleMessage("24 horas predefinidas"), + "share" : MessageLookupByLibrary.simpleMessage("Partilhar"), + "showNotesFonts" : MessageLookupByLibrary.simpleMessage("Mostrar tipo de letra das notas"), + "size" : MessageLookupByLibrary.simpleMessage("Tamanho"), + "skipSecondsAtEnd" : MessageLookupByLibrary.simpleMessage("Saltar segundos no fim"), + "skipSecondsAtStart" : MessageLookupByLibrary.simpleMessage("Saltar segundos no início"), + "skipSilence" : MessageLookupByLibrary.simpleMessage("Saltar silêncio"), + "skipToNext" : MessageLookupByLibrary.simpleMessage("Saltar para o próximo"), + "sleepTimer" : MessageLookupByLibrary.simpleMessage("Temporizador"), + "status" : MessageLookupByLibrary.simpleMessage("Status"), + "stop" : MessageLookupByLibrary.simpleMessage("Parar"), + "subscribe" : MessageLookupByLibrary.simpleMessage("Subscrever"), + "subscribeExportDes" : MessageLookupByLibrary.simpleMessage("Exportar ficheiro OPML de todos os podcasts"), + "syncNow" : MessageLookupByLibrary.simpleMessage("Sync now"), + "systemDefault" : MessageLookupByLibrary.simpleMessage("Predefinido do sistema"), + "timeLastPlayed" : m26, + "timeLeft" : m27, + "to" : m28, + "toastAddPlaylist" : MessageLookupByLibrary.simpleMessage("Adicionado à lista de reprodução"), + "toastDiscovery" : MessageLookupByLibrary.simpleMessage("Característica \"Descobrir\" ligada, por favor reinicia a aplicação"), + "toastFileError" : MessageLookupByLibrary.simpleMessage("Erro no ficheiro, subscrição falhou"), + "toastFileNotValid" : MessageLookupByLibrary.simpleMessage("Ficheiro inválido"), + "toastHomeGroupNotSupport" : MessageLookupByLibrary.simpleMessage("Grupo Home não é suportado"), + "toastImportSettingsSuccess" : MessageLookupByLibrary.simpleMessage("Definições importadas com sucesso"), + "toastOneGroup" : MessageLookupByLibrary.simpleMessage("Seleciona pelo menos um grupo"), + "toastPodcastRecovering" : MessageLookupByLibrary.simpleMessage("A recuperar, espera um momento"), + "toastReadFile" : MessageLookupByLibrary.simpleMessage("Ficheiro lido com sucesso"), + "toastRecoverFailed" : MessageLookupByLibrary.simpleMessage("Recuperação do podcast falhou"), + "toastRemovePlaylist" : MessageLookupByLibrary.simpleMessage("Episódio removido da lista de reprodução"), + "toastSettingSaved" : MessageLookupByLibrary.simpleMessage("Definições guardadas"), + "toastTimeEqualEnd" : MessageLookupByLibrary.simpleMessage("Tempo marcado é igual ao tempo de fim"), + "toastTimeEqualStart" : MessageLookupByLibrary.simpleMessage("Tempo marcado é igual ao tempo de início"), + "translators" : MessageLookupByLibrary.simpleMessage("Tradutores"), + "understood" : MessageLookupByLibrary.simpleMessage("Compreendido"), + "undo" : MessageLookupByLibrary.simpleMessage("DESFAZER"), + "unlike" : MessageLookupByLibrary.simpleMessage("Não gosto"), + "unliked" : MessageLookupByLibrary.simpleMessage("Episódio removido dos favoritos"), + "updateDate" : MessageLookupByLibrary.simpleMessage("Atualizar data"), + "updateEpisodesCount" : m29, + "updateFailed" : MessageLookupByLibrary.simpleMessage("Atuallização falhou, erro de conexão"), + "username" : MessageLookupByLibrary.simpleMessage("Username"), + "usernameRequired" : MessageLookupByLibrary.simpleMessage("Username requeired"), + "version" : m30 + }; +} diff --git a/lib/generated/intl/messages_zh-Hans.dart b/lib/generated/intl/messages_zh-Hans.dart index fbf8493..54cb7df 100644 --- a/lib/generated/intl/messages_zh-Hans.dart +++ b/lib/generated/intl/messages_zh-Hans.dart @@ -39,43 +39,47 @@ class MessageLookup extends MessageLookupByLibrary { static m9(count) => "${Intl.plural(count, zero: '0小时', other: '${count} 小时')}"; - static m10(count) => "${Intl.plural(count, zero: '刚刚', other: '${count}分钟前')}"; + static m10(service) => "绑定 ${service}"; - static m11(count) => "${Intl.plural(count, zero: '0分钟', other: '${count}分钟')}"; + static m11(userName) => "使用${userName}登入"; - static m12(title) => "获取数据 ${title}"; + static m12(count) => "${Intl.plural(count, zero: '刚刚', other: '${count}分钟前')}"; - static m13(title) => "订阅失败,网络错误 ${title}"; + static m13(count) => "${Intl.plural(count, zero: '0分钟', other: '${count}分钟')}"; - static m14(title) => "订阅 ${title}"; + static m14(title) => "获取数据 ${title}"; - static m15(title) => "订阅失败,播客已存在 ${title}"; + static m15(title) => "订阅失败,网络错误 ${title}"; - static m16(title) => "订阅成功 ${title}"; + static m16(title) => "订阅 ${title}"; - static m17(title) => "更新 ${title}"; + static m17(title) => "订阅失败,播客已存在 ${title}"; - static m18(title) => "更新失败 ${title}"; + static m18(title) => "订阅成功 ${title}"; - static m19(count) => "${Intl.plural(count, zero: '', other: '播客')}"; + static m19(title) => "更新 ${title}"; - static m20(date) => "${date}上线"; + static m20(title) => "更新失败 ${title}"; - static m21(date) => "${date}移除"; + static m21(count) => "${Intl.plural(count, zero: '', other: '播客')}"; - static m22(count) => "${Intl.plural(count, zero: '0 秒', other: '${count} 秒')}"; + static m22(date) => "${date}上线"; - static m23(count) => "${Intl.plural(count, zero: '刚刚', other: '${count}秒前')}"; + static m23(date) => "${date}移除"; - static m24(time) => "上次播放${time}"; + static m24(count) => "${Intl.plural(count, zero: '0 秒', other: '${count} 秒')}"; - static m25(time) => "剩余 ${time}"; + static m25(count) => "${Intl.plural(count, zero: '刚刚', other: '${count}秒前')}"; - static m26(time) => "到${time}"; + static m26(time) => "上次播放${time}"; - static m27(count) => "${Intl.plural(count, zero: '未有更新', other: '更新 ${count} 集节目')}"; + static m27(time) => "剩余 ${time}"; - static m28(version) => "版本:${version}"; + static m28(time) => "到${time}"; + + static m29(count) => "${Intl.plural(count, zero: '未有更新', other: '更新 ${count} 集节目')}"; + + static m30(version) => "版本:${version}"; final messages = _notInlinedMessages(_notInlinedMessages); static _notInlinedMessages(_) => { @@ -140,6 +144,7 @@ class MessageLookup extends MessageLookupByLibrary { "fonts" : MessageLookupByLibrary.simpleMessage("字体"), "from" : m5, "goodNight" : MessageLookupByLibrary.simpleMessage("晚安"), + "gpodderLoginDes" : MessageLookupByLibrary.simpleMessage("恭喜!您已经成功绑定 gpodder.net 账号,Tsacdop 将会自动同步您的订阅到 gpodder.net 账户。"), "groupExisted" : MessageLookupByLibrary.simpleMessage("组名已使用"), "groupFilter" : MessageLookupByLibrary.simpleMessage("分组"), "groupRemoveConfirm" : MessageLookupByLibrary.simpleMessage("您确认要移除该分组吗?播客将被移动到 Home 分组。"), @@ -157,9 +162,12 @@ class MessageLookup extends MessageLookupByLibrary { "hoursAgo" : m8, "hoursCount" : m9, "import" : MessageLookupByLibrary.simpleMessage("导入"), + "intergateWith" : m10, "introFourthPage" : MessageLookupByLibrary.simpleMessage("您可以长按节目打开快捷菜单。"), "introSecondPage" : MessageLookupByLibrary.simpleMessage("您可以通过搜索订阅播客,也可以直接导入OPML文件。"), "introThirdPage" : MessageLookupByLibrary.simpleMessage("您可以创建分组,上下滑动切换分组。"), + "invalidName" : MessageLookupByLibrary.simpleMessage("用户名错误"), + "lastUpdate" : MessageLookupByLibrary.simpleMessage("最近更新"), "later" : MessageLookupByLibrary.simpleMessage("稍后"), "lightMode" : MessageLookupByLibrary.simpleMessage("明亮模式"), "like" : MessageLookupByLibrary.simpleMessage("喜欢"), @@ -168,6 +176,10 @@ class MessageLookup extends MessageLookupByLibrary { "listen" : MessageLookupByLibrary.simpleMessage("收听"), "listened" : MessageLookupByLibrary.simpleMessage("已收听"), "loadMore" : MessageLookupByLibrary.simpleMessage("加载更多"), + "loggedInAs" : m11, + "login" : MessageLookupByLibrary.simpleMessage("登入"), + "loginFailed" : MessageLookupByLibrary.simpleMessage("登入失败"), + "logout" : MessageLookupByLibrary.simpleMessage("注销"), "mark" : MessageLookupByLibrary.simpleMessage("标记"), "markConfirm" : MessageLookupByLibrary.simpleMessage("确认标记"), "markConfirmContent" : MessageLookupByLibrary.simpleMessage("是否确认标记全部节目为已收听?"), @@ -178,8 +190,8 @@ class MessageLookup extends MessageLookupByLibrary { "menuMarkAllListened" : MessageLookupByLibrary.simpleMessage("标记所有已收听"), "menuViewRSS" : MessageLookupByLibrary.simpleMessage("查看 RSS"), "menuVisitSite" : MessageLookupByLibrary.simpleMessage("访问网站"), - "minsAgo" : m10, - "minsCount" : m11, + "minsAgo" : m12, + "minsCount" : m13, "network" : MessageLookupByLibrary.simpleMessage("网络"), "newGroup" : MessageLookupByLibrary.simpleMessage("创建分组"), "newestFirst" : MessageLookupByLibrary.simpleMessage("由新到旧"), @@ -189,15 +201,17 @@ class MessageLookup extends MessageLookupByLibrary { "noEpisodeRecent" : MessageLookupByLibrary.simpleMessage("暂无节目"), "noPodcastGroup" : MessageLookupByLibrary.simpleMessage("分组无播客"), "noShownote" : MessageLookupByLibrary.simpleMessage("节目简介暂未收到。"), - "notificaitonFatch" : m12, - "notificationNetworkError" : m13, + "notificaitonFatch" : m14, + "notificationNetworkError" : m15, "notificationSetting" : MessageLookupByLibrary.simpleMessage("通知栏"), - "notificationSubscribe" : m14, - "notificationSubscribeExisted" : m15, - "notificationSuccess" : m16, - "notificationUpdate" : m17, - "notificationUpdateError" : m18, + "notificationSubscribe" : m16, + "notificationSubscribeExisted" : m17, + "notificationSuccess" : m18, + "notificationUpdate" : m19, + "notificationUpdateError" : m20, "oldestFirst" : MessageLookupByLibrary.simpleMessage("由旧到新"), + "passwdRequired" : MessageLookupByLibrary.simpleMessage("密码为空"), + "password" : MessageLookupByLibrary.simpleMessage("密码"), "pause" : MessageLookupByLibrary.simpleMessage("暂停"), "play" : MessageLookupByLibrary.simpleMessage("播放"), "playback" : MessageLookupByLibrary.simpleMessage("播放控制"), @@ -207,7 +221,7 @@ class MessageLookup extends MessageLookupByLibrary { "playerHeightTall" : MessageLookupByLibrary.simpleMessage("高"), "playing" : MessageLookupByLibrary.simpleMessage("正在播放"), "plugins" : MessageLookupByLibrary.simpleMessage("插件"), - "podcast" : m19, + "podcast" : m21, "podcastSubscribed" : MessageLookupByLibrary.simpleMessage("播客已订阅"), "popupMenuDownloadDes" : MessageLookupByLibrary.simpleMessage("下载节目"), "popupMenuLaterDes" : MessageLookupByLibrary.simpleMessage("添加到播放列表"), @@ -215,7 +229,7 @@ class MessageLookup extends MessageLookupByLibrary { "popupMenuMarkDes" : MessageLookupByLibrary.simpleMessage("设置为已收听"), "popupMenuPlayDes" : MessageLookupByLibrary.simpleMessage("播放节目"), "privacyPolicy" : MessageLookupByLibrary.simpleMessage("隐私条款"), - "published" : m20, + "published" : m22, "publishedDaily" : MessageLookupByLibrary.simpleMessage("每日更新"), "publishedMonthly" : MessageLookupByLibrary.simpleMessage("每月更新"), "publishedWeekly" : MessageLookupByLibrary.simpleMessage("每周更新"), @@ -225,15 +239,15 @@ class MessageLookup extends MessageLookupByLibrary { "remove" : MessageLookupByLibrary.simpleMessage("移除"), "removeConfirm" : MessageLookupByLibrary.simpleMessage("取消订阅"), "removePodcastDes" : MessageLookupByLibrary.simpleMessage("您确认要取消订阅吗?"), - "removedAt" : m21, + "removedAt" : m23, "save" : MessageLookupByLibrary.simpleMessage("保存"), "schedule" : MessageLookupByLibrary.simpleMessage("定时"), "search" : MessageLookupByLibrary.simpleMessage("搜索"), "searchEpisode" : MessageLookupByLibrary.simpleMessage("搜索节目"), "searchInvalidRss" : MessageLookupByLibrary.simpleMessage("RSS 链接错误"), "searchPodcast" : MessageLookupByLibrary.simpleMessage("搜索播客"), - "secCount" : m22, - "secondsAgo" : m23, + "secCount" : m24, + "secondsAgo" : m25, "settingStorage" : MessageLookupByLibrary.simpleMessage("储存空间"), "settings" : MessageLookupByLibrary.simpleMessage("设置"), "settingsAccentColor" : MessageLookupByLibrary.simpleMessage("次要颜色"), @@ -313,13 +327,15 @@ class MessageLookup extends MessageLookupByLibrary { "skipSilence" : MessageLookupByLibrary.simpleMessage("跳过无声"), "skipToNext" : MessageLookupByLibrary.simpleMessage("下一首"), "sleepTimer" : MessageLookupByLibrary.simpleMessage("睡眠模式"), + "status" : MessageLookupByLibrary.simpleMessage("状态"), "stop" : MessageLookupByLibrary.simpleMessage("停止"), "subscribe" : MessageLookupByLibrary.simpleMessage("订阅"), "subscribeExportDes" : MessageLookupByLibrary.simpleMessage("导出 OPML 文件"), + "syncNow" : MessageLookupByLibrary.simpleMessage("立即同步"), "systemDefault" : MessageLookupByLibrary.simpleMessage("系统默认"), - "timeLastPlayed" : m24, - "timeLeft" : m25, - "to" : m26, + "timeLastPlayed" : m26, + "timeLeft" : m27, + "to" : m28, "toastAddPlaylist" : MessageLookupByLibrary.simpleMessage("添加到播放列表"), "toastDiscovery" : MessageLookupByLibrary.simpleMessage("重启应用后可查看"), "toastFileError" : MessageLookupByLibrary.simpleMessage("文件错误,导入失败"), @@ -340,8 +356,10 @@ class MessageLookup extends MessageLookupByLibrary { "unlike" : MessageLookupByLibrary.simpleMessage("取消喜欢"), "unliked" : MessageLookupByLibrary.simpleMessage("从收藏移除"), "updateDate" : MessageLookupByLibrary.simpleMessage("更新日期"), - "updateEpisodesCount" : m27, + "updateEpisodesCount" : m29, "updateFailed" : MessageLookupByLibrary.simpleMessage("更新失败"), - "version" : m28 + "username" : MessageLookupByLibrary.simpleMessage("用户名"), + "usernameRequired" : MessageLookupByLibrary.simpleMessage("用户名为空"), + "version" : m30 }; } diff --git a/lib/home/home_menu.dart b/lib/home/home_menu.dart index 4407a9f..053dea5 100644 --- a/lib/home/home_menu.dart +++ b/lib/home/home_menu.dart @@ -11,7 +11,7 @@ import 'package:line_icons/line_icons.dart'; import 'package:provider/provider.dart'; import '../local_storage/key_value_storage.dart'; -import '../service/ompl_build.dart'; +import '../service/opml_build.dart'; import '../settings/settting.dart'; import '../state/podcast_group.dart'; import '../state/refresh_podcast.dart'; @@ -58,7 +58,8 @@ class _PopupMenuState extends State { final s = context.s; var file = File(path); try { - Map> data = PodcastsBackup.parseOMPL(file); + final opml = file.readAsStringSync(); + Map> data = PodcastsBackup.parseOMPL(opml); for (var entry in data.entries) { var title = entry.key; var list = entry.value.reversed; @@ -83,14 +84,16 @@ class _PopupMenuState extends State { void _getFilePath() async { final s = context.s; try { - var filePath = await FilePicker.getFilePath(type: FileType.any); - if (filePath == '') { + var filePickResult = + await FilePicker.platform.pickFiles(type: FileType.any); + if (filePickResult == null) { return; } Fluttertoast.showToast( msg: s.toastReadFile, gravity: ToastGravity.TOP, ); + final filePath = filePickResult.files.first.path; _saveOmpl(filePath); } on PlatformException catch (e) { developer.log(e.toString(), name: 'Get OMPL file'); diff --git a/lib/home/pocast_discovery.dart b/lib/home/pocast_discovery.dart index 521ebb8..db28edd 100644 --- a/lib/home/pocast_discovery.dart +++ b/lib/home/pocast_discovery.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../local_storage/key_value_storage.dart'; -import '../service/api_search.dart'; +import '../service/search_api.dart'; import '../state/search_state.dart'; -import '../type/search_genre.dart'; -import '../type/searchpodcast.dart'; +import '../type/search_api/search_genre.dart'; +import '../type/search_api/searchpodcast.dart'; import '../util/custom_widget.dart'; import '../util/extension_helper.dart'; import 'search_podcast.dart'; @@ -113,7 +113,7 @@ class DiscoveryPageState extends State { )); Future> _getTopPodcasts({int page}) async { - final searchEngine = SearchEngine(); + final searchEngine = ListenNotesSearch(); var searchResult = await searchEngine.fetchBestPodcast( genre: '', page: page, @@ -141,13 +141,12 @@ class DiscoveryPageState extends State { if (snapshot.hasData && snapshot.data.isNotEmpty) { final history = snapshot.data; return SizedBox( - height: 50, child: Wrap( direction: Axis.horizontal, children: history .map((e) => Padding( padding: const EdgeInsets.fromLTRB( - 8, 8, 0, 8), + 8, 2, 0, 0), child: FlatButton.icon( color: Colors .accents[history.indexOf(e)] @@ -159,7 +158,7 @@ class DiscoveryPageState extends State { onPressed: () => widget.onTap(e), label: Text(e), icon: Icon( - Icons.bookmark_border, + Icons.search, size: 20, ), ), @@ -313,7 +312,7 @@ class __TopPodcastListState extends State<_TopPodcastList> { bool _loading; int _page; Future> _getTopPodcasts({Genre genre, int page}) async { - final searchEngine = SearchEngine(); + final searchEngine = ListenNotesSearch(); var searchResult = await searchEngine.fetchBestPodcast( genre: genre.id, page: page, diff --git a/lib/home/search_podcast.dart b/lib/home/search_podcast.dart index b24be99..a3834d8 100644 --- a/lib/home/search_podcast.dart +++ b/lib/home/search_podcast.dart @@ -11,11 +11,11 @@ import 'package:provider/provider.dart'; import 'package:webfeed/webfeed.dart'; import '../local_storage/key_value_storage.dart'; -import '../service/api_search.dart'; +import '../service/search_api.dart'; import '../state/podcast_group.dart'; import '../state/search_state.dart'; -import '../type/searchepisodes.dart'; -import '../type/searchpodcast.dart'; +import '../type/search_api/searchepisodes.dart'; +import '../type/search_api/searchpodcast.dart'; import '../util/extension_helper.dart'; import 'pocast_discovery.dart'; @@ -359,7 +359,7 @@ class _SearchListState extends State { Future> _getList( String searchText, int nextOffset) async { if (nextOffset == 0) _saveHistory(searchText); - final searchEngine = SearchEngine(); + final searchEngine = ListenNotesSearch(); var searchResult = await searchEngine.searchPodcasts( searchText: searchText, nextOffset: nextOffset); _offset = searchResult.nextOffset; @@ -562,7 +562,7 @@ class _SearchResultDetailState extends State Future> _getEpisodes( {String id, int nextEpisodeDate}) async { - var searchEngine = SearchEngine(); + var searchEngine = ListenNotesSearch(); var searchResult = await searchEngine.fetchEpisode( id: id, nextEpisodeDate: nextEpisodeDate); _nextEpisdoeDate = searchResult.nextEpisodeDate; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index ed308f1..ddb62af 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -140,6 +140,8 @@ }, "goodNight": "Good Night", "@goodNight": {}, + "gpodderLoginDes": "Congratulations! You have linked gpodder.net account successfully. Tsacdop will automatically sync subscriptions on your device with your gpodder.net account.", + "@gpodderLoginDes": {}, "groupExisted": "Group already exists", "@groupExisted": { "description": "Group name validate in add group dialog. User can't add group with same name." @@ -180,12 +182,25 @@ "@hoursCount": {}, "import": "Import", "@import": {}, + "intergateWith": "Integrate with {service}", + "@intergateWith": { + "description": "Integrate with", + "placeholders": { + "service": {} + } + }, "introFourthPage": "You can long press on episode card for quick actions.", "@introFourthPage": {}, "introSecondPage": "Subscribe podcast via search or import OPML file.", "@introSecondPage": {}, "introThirdPage": "You can create new group for podcasts.", "@introThirdPage": {}, + "invalidName": "Invalid username", + "@invalidName": {}, + "lastUpdate": "Last update", + "@lastUpdate": { + "description": "gpodder.net update" + }, "later": "Later", "@later": {}, "lightMode": "Light mode", @@ -204,6 +219,23 @@ "@listened": {}, "loadMore": "Load more", "@loadMore": {}, + "loggedInAs": "Logged in as {userName}", + "@loggedInAs": { + "description": "gpodder.net", + "placeholders": { + "userName": {} + } + }, + "login": "Login", + "@login": { + "description": "gpodder.net login" + }, + "loginFailed": "Login failed", + "@loginFailed": {}, + "logout": "Logout", + "@logout": { + "description": "gpodder.net logout" + }, "mark": "Mark", "@mark": { "description": "In listen history page, if a episode is marked as listened." @@ -292,6 +324,10 @@ }, "oldestFirst": "Oldest first", "@oldestFirst": {}, + "passwdRequired": "Password required", + "@passwdRequired": {}, + "password": "Password", + "@password": {}, "pause": "Pause", "@pause": {}, "play": "Play", @@ -547,12 +583,18 @@ "@skipToNext": {}, "sleepTimer": "Sleep timer", "@sleepTimer": {}, + "status": "Status", + "@status": { + "description": "gpodder.net status" + }, "stop": "Stop", "@stop": {}, "subscribe": "Subscribe", "@subscribe": {}, "subscribeExportDes": "Export OPML file of all podcasts", "@subscribeExportDes": {}, + "syncNow": "Sync now", + "@syncNow": {}, "systemDefault": "System default", "@systemDefault": {}, "timeLastPlayed": "Last time {time}", @@ -628,6 +670,10 @@ "@updateEpisodesCount": {}, "updateFailed": "Update failed, network error", "@updateFailed": {}, + "username": "Username", + "@username": {}, + "usernameRequired": "Username required", + "@usernameRequired": {}, "version": "Version: {version}", "@version": { "placeholders": { diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 710cf4a..2cc1d8e 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -140,6 +140,8 @@ }, "goodNight": "Buenas noches", "@goodNight": {}, + "gpodderLoginDes": "Congratulations! You have linked gpodder.net account successfully. Tsacdop will automatically sync subscriptions on your device with your gpodder.net account.", + "@gpodderLoginDes": {}, "groupExisted": "El grupo ya existe", "@groupExisted": { "description": "Group name validate in add group dialog. User can't add group with same name." @@ -180,12 +182,25 @@ "@hoursCount": {}, "import": "Importar", "@import": {}, + "intergateWith": "Integrate with {service}", + "@intergateWith": { + "description": "Integrate with", + "placeholders": { + "service": {} + } + }, "introFourthPage": "Puedes mantener presionado un episodio para realizar acciones rápidas", "@introFourthPage": {}, "introSecondPage": "Suscribete a podcasts buscándolos, o importando un archivo OPML", "@introSecondPage": {}, "introThirdPage": "Puedes crear un nuevo grupo de podcasts", "@introThirdPage": {}, + "invalidName": "Invalid username", + "@invalidName": {}, + "lastUpdate": "Last update", + "@lastUpdate": { + "description": "gpodder.net update" + }, "later": "Despues", "@later": {}, "lightMode": "Modo claro", @@ -204,6 +219,23 @@ "@listened": {}, "loadMore": "Cargar mas", "@loadMore": {}, + "loggedInAs": "Logged in as {userName}", + "@loggedInAs": { + "description": "gpodder.net", + "placeholders": { + "userName": {} + } + }, + "login": "Loign", + "@login": { + "description": "gpodder.net login" + }, + "loginFailed": "Login failed", + "@loginFailed": {}, + "logout": "Logout", + "@logout": { + "description": "gpodder.net logout" + }, "mark": "Completado", "@mark": { "description": "In listen history page, if a episode is marked as listened." @@ -292,6 +324,10 @@ }, "oldestFirst": "Mas antiguos primero", "@oldestFirst": {}, + "passwdRequired": "Password required", + "@passwdRequired": {}, + "password": "Password", + "@password": {}, "pause": "Pause", "@pause": {}, "play": "Reproducir", @@ -547,12 +583,18 @@ "@skipToNext": {}, "sleepTimer": "Temporizador de sueño", "@sleepTimer": {}, + "status": "Status", + "@status": { + "description": "gpodder.net status" + }, "stop": "Stop", "@stop": {}, "subscribe": "Suscribir", "@subscribe": {}, "subscribeExportDes": "Exportar OPML de todos los podcasts", "@subscribeExportDes": {}, + "syncNow": "Sync now", + "@syncNow": {}, "systemDefault": "Acorde al sistema", "@systemDefault": {}, "timeLastPlayed": "Tiempo previo {time}", @@ -628,6 +670,10 @@ "@updateEpisodesCount": {}, "updateFailed": "Actualización fallida, error de red", "@updateFailed": {}, + "username": "Username", + "@username": {}, + "usernameRequired": "Username required", + "@usernameRequired": {}, "version": "Versión: {version}", "@version": { "placeholders": { diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index dc5e6ad..81fc839 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -140,6 +140,8 @@ }, "goodNight": "Bonne nuit", "@goodNight": {}, + "gpodderLoginDes": "Congratulations! You have linked gpodder.net account successfully. Tsacdop will automatically sync subscriptions on your device with your gpodder.net account.", + "@gpodderLoginDes": {}, "groupExisted": "Ce groupe existe déjà", "@groupExisted": { "description": "Group name validate in add group dialog. User can't add group with same name." @@ -180,12 +182,25 @@ "@hoursCount": {}, "import": "Importer", "@import": {}, + "intergateWith": "Integrate with {service}", + "@intergateWith": { + "description": "Integrate with", + "placeholders": { + "service": {} + } + }, "introFourthPage": "Un appui long sur un épisode lance les actions rapides.", "@introFourthPage": {}, "introSecondPage": "S'abonner aux podcasts via la section recherche ou un fichier OPML.", "@introSecondPage": {}, "introThirdPage": "Vous pouvez créer des groupes de podcasts.", "@introThirdPage": {}, + "invalidName": "Invalid username", + "@invalidName": {}, + "lastUpdate": "Last update", + "@lastUpdate": { + "description": "gpodder.net update" + }, "later": "Plus tard", "@later": {}, "lightMode": "Mode clair", @@ -204,6 +219,23 @@ "@listened": {}, "loadMore": "Voir plus", "@loadMore": {}, + "loggedInAs": "Logged in as {userName}", + "@loggedInAs": { + "description": "gpodder.net", + "placeholders": { + "userName": {} + } + }, + "login": "Login", + "@login": { + "description": "gpodder.net login" + }, + "loginFailed": "Login failed", + "@loginFailed": {}, + "logout": "Logout", + "@logout": { + "description": "gpodder.net logout" + }, "mark": "✓", "@mark": { "description": "In listen history page, if a episode is marked as listened." @@ -292,6 +324,10 @@ }, "oldestFirst": "Le plus ancien en premier", "@oldestFirst": {}, + "passwdRequired": "Password required", + "@passwdRequired": {}, + "password": "Password", + "@password": {}, "pause": "Pause", "@pause": {}, "play": "Lecture", @@ -547,12 +583,18 @@ "@skipToNext": {}, "sleepTimer": "Minuterie", "@sleepTimer": {}, + "status": "Status", + "@status": { + "description": "gpodder.net status" + }, "stop": "Stop", "@stop": {}, "subscribe": "S'abonner", "@subscribe": {}, "subscribeExportDes": "Exporter le fichier OPML de tous les podcasts.", "@subscribeExportDes": {}, + "syncNow": "Sync now", + "@syncNow": {}, "systemDefault": "Système par défaut", "@systemDefault": {}, "timeLastPlayed": "Dernière écoute à {time}", @@ -628,6 +670,10 @@ "@updateEpisodesCount": {}, "updateFailed": "Échec de la mise à jour, erreur réseau", "@updateFailed": {}, + "username": "Username", + "@username": {}, + "usernameRequired": "Username required", + "@usernameRequired": {}, "version": "Version : {version}", "@version": { "placeholders": { diff --git a/lib/l10n/intl_pt.arb b/lib/l10n/intl_pt.arb new file mode 100644 index 0000000..f58aab4 --- /dev/null +++ b/lib/l10n/intl_pt.arb @@ -0,0 +1,683 @@ +{ + "@@locale": "pt", + "add": "Adicionar", + "@add": { + "description": "Subscribe new podcast" + }, + "addEpisodeGroup": "{count, plural, zero{} one{{count} episódio de {groupName} adicionado à lista} other{{count} episódios de {groupName} adicionados à lista}}", + "@addEpisodeGroup": { + "placeholders": { + "groupName": {} + } + }, + "addNewEpisodeAll": "{count, plural, zero{} one{{count} episódio adicionado à lista} other{{count} episódios adicionados à lista}}", + "@addNewEpisodeAll": {}, + "addNewEpisodeTooltip": "Adiciona novos episódios à lista de reprodução", + "@addNewEpisodeTooltip": {}, + "addSomeGroups": "Adiciona alguns grupos", + "@addSomeGroups": { + "description": "Please add new groups" + }, + "all": "Todos", + "@all": {}, + "autoDownload": "Download automático", + "@autoDownload": {}, + "back": "Atrás", + "@back": {}, + "boostVolume": "Aumentar volume", + "@boostVolume": { + "description": "Boost volume in player widget." + }, + "buffering": "A carregar", + "@buffering": {}, + "cancel": "CANCELAR", + "@cancel": {}, + "cellularConfirm": "Alerta de dados móveis", + "@cellularConfirm": {}, + "cellularConfirmDes": "Tens a certeza que queres usar dados móveis para downloads?", + "@cellularConfirmDes": {}, + "changeLayout": "Mudar aparência", + "@changeLayout": {}, + "changelog": "Registo de mudanças", + "@changelog": {}, + "chooseA": "Escolher um", + "@chooseA": {}, + "clear": "Limpar", + "@clear": {}, + "color": "Cor", + "@color": {}, + "confirm": "CONFIRMAR", + "@confirm": {}, + "darkMode": "Modo escuro", + "@darkMode": {}, + "daysAgo": "{count, plural, zero{Hoje} one{Há {count} dia} other{Há {count} dias}}", + "@daysAgo": {}, + "daysCount": "{count, plural, zero{Nunca} one{{count} dia} other{{count} dias}}", + "@daysCount": {}, + "delete": "Eliminar", + "@delete": {}, + "developer": "Desenvolvedor", + "@developer": { + "description": "Can also translate to About me" + }, + "dismiss": "Minimizar", + "@dismiss": {}, + "done": "Feito", + "@done": {}, + "download": "Download", + "@download": {}, + "downloaded": "Descarregado", + "@downloaded": {}, + "downloadRemovedToast": "Download removido", + "@downloadRemovedToast": {}, + "editGroupName": "Editar nome do grupo", + "@editGroupName": {}, + "endOfEpisode": "Fim do episódio", + "@endOfEpisode": {}, + "episode": "{count, plural, zero{} one{Episódio} other{Episódios}}", + "@episode": {}, + "fastForward": "Avanço", + "@fastForward": {}, + "fastRewind": "Recuo rápido", + "@fastRewind": {}, + "featureDiscoveryEditGroup": "Prime para editar grupo", + "@featureDiscoveryEditGroup": {}, + "featureDiscoveryEditGroupDes": "Podes alterar o nome do grupo ou apagá-lo aqui, mas o grupo Home não pode ser editado ou eliminado", + "@featureDiscoveryEditGroupDes": {}, + "featureDiscoveryEpisode": "Vista de episódios", + "@featureDiscoveryEpisode": {}, + "featureDiscoveryEpisodeDes": "Podes manter premido para reproduzir um episódio ou adicioná-lo a uma lista de reprodução.", + "@featureDiscoveryEpisodeDes": {}, + "featureDiscoveryEpisodeTitle": "Mantém premido para reproduzir um episódio instantâneamente", + "@featureDiscoveryEpisodeTitle": {}, + "featureDiscoveryGroup": "Prime para adicionar grupo", + "@featureDiscoveryGroup": {}, + "featureDiscoveryGroupDes": "O grupo por defeito para novos podcasts é Home. Podes criar novos grupos e mover os podcasts para estes, assim como adicionar podcasts a múltiplos grupos.", + "@featureDiscoveryGroupDes": {}, + "featureDiscoveryGroupPodcast": "Mantém premido para reordenar podcasts", + "@featureDiscoveryGroupPodcast": {}, + "featureDiscoveryGroupPodcastDes": "Podes premir para ver mais opções, ou manter premido para reordenar podcasts em grupos.", + "@featureDiscoveryGroupPodcastDes": {}, + "featureDiscoveryOMPL": "Premir para importar um OPML", + "@featureDiscoveryOMPL": {}, + "featureDiscoveryOMPLDes": "Podes importar ficheiros OPML, abrir as definições ou atualizar todos os podcasts aqui.", + "@featureDiscoveryOMPLDes": {}, + "featureDiscoveryPlaylist": "Prime para abrir a lista de reprodução", + "@featureDiscoveryPlaylist": {}, + "featureDiscoveryPlaylistDes": "Podes adicionar episódios à lista de reprodução manualmente. Os episódios serão automaticamente removidos das listas de reprodução quando reproduzidos.", + "@featureDiscoveryPlaylistDes": {}, + "featureDiscoveryPodcast": "Vista do podcast", + "@featureDiscoveryPodcast": {}, + "featureDiscoveryPodcastDes": "Podes premir \"Ver Todos\" para adicionar grupos ou organizar pdcasts.", + "@featureDiscoveryPodcastDes": {}, + "featureDiscoveryPodcastTitle": "Deslizar verticalmente para alterar grupos", + "@featureDiscoveryPodcastTitle": {}, + "featureDiscoverySearch": "Prime para procurar podcasts", + "@featureDiscoverySearch": {}, + "featureDiscoverySearchDes": "Podes procurar pelo título do podcast, palavra-chave ou ligação RSS para subscrever novos podcasts.", + "@featureDiscoverySearchDes": {}, + "feedbackEmail": "Escreve-me", + "@feedbackEmail": {}, + "feedbackGithub": "Submeter problema", + "@feedbackGithub": {}, + "feedbackPlay": "Avaliar na Play Store", + "@feedbackPlay": { + "description": "Rate on Google Play Store.\nUser can tap to open play link." + }, + "feedbackTelegram": "Juntar um grupo", + "@feedbackTelegram": {}, + "filter": "Filtro", + "@filter": {}, + "fonts": "Fontes", + "@fonts": {}, + "fontStyle": "Estilo do tipo de letra", + "@fontStyle": {}, + "from": "De {time}", + "@from": { + "placeholders": { + "time": {} + } + }, + "goodNight": "Boa Noite", + "@goodNight": {}, + "gpodderLoginDes": "Congratulations! You have linked gpodder.net account successfully. Tsacdop will automatically sync subscriptions on your device with your gpodder.net account.", + "@gpodderLoginDes": {}, + "groupExisted": "Grupo já existe", + "@groupExisted": { + "description": "Group name validate in add group dialog. User can't add group with same name." + }, + "groupFilter": "Filtro de grupo", + "@groupFilter": {}, + "groupRemoveConfirm": "Tens a certeza que queres eliminar este grupo? Os podcasts serão removidos para o grupo \"Home\".", + "@groupRemoveConfirm": {}, + "groups": "{count, plural, zero{Grupo} one{Grupo} other{Grupos}}", + "@groups": {}, + "hideListenedSetting": "Esconder ouvidos", + "@hideListenedSetting": {}, + "homeGroupsSeeAll": "Ver Todos", + "@homeGroupsSeeAll": {}, + "homeMenuPlaylist": "Lista de Reprodução", + "@homeMenuPlaylist": {}, + "homeSubMenuSortBy": "Ordenar por", + "@homeSubMenuSortBy": {}, + "homeTabMenuFavotite": "Favorito", + "@homeTabMenuFavotite": {}, + "homeTabMenuRecent": "Recentes", + "@homeTabMenuRecent": {}, + "homeToprightMenuAbout": "Sobre", + "@homeToprightMenuAbout": {}, + "homeToprightMenuImportOMPL": "Importar OPML", + "@homeToprightMenuImportOMPL": {}, + "homeToprightMenuRefreshAll": "Atualizar todos", + "@homeToprightMenuRefreshAll": {}, + "hostedOn": "Hospedado em {host}", + "@hostedOn": { + "placeholders": { + "host": {} + } + }, + "hoursAgo": "{count, plural, zero{} one{há {count} hora} other{há {count} horas}}", + "@hoursAgo": {}, + "hoursCount": "{count, plural, zero{0 horas} one{{count} hora} other{{count} horas}}", + "@hoursCount": {}, + "import": "Importar", + "@import": {}, + "intergateWith": "Integrate with {service}", + "@intergateWith": { + "description": "Integrate with", + "placeholders": { + "service": {} + } + }, + "introFourthPage": "Podes manter premido um episódio para uma ação rápida.", + "@introFourthPage": {}, + "introSecondPage": "Subscreve podcasts por pesquisa ou importa um ficheiro OPML.", + "@introSecondPage": {}, + "introThirdPage": "Podes criar um novo grupo para podcasts.", + "@introThirdPage": {}, + "invalidName": "Invalid username", + "@invalidName": {}, + "lastUpdate": "Last update", + "@lastUpdate": { + "description": "gpodder.net update" + }, + "later": "Mais tarde", + "@later": {}, + "lightMode": "Modo claro", + "@lightMode": {}, + "like": "Gosto", + "@like": {}, + "liked": "Gostou", + "@liked": {}, + "likeDate": "Data do Gosto", + "@likeDate": { + "description": "Favorite tab, sort by like date." + }, + "listen": "Ouvir", + "@listen": {}, + "listened": "Ouvido", + "@listened": {}, + "loadMore": "Carregar mais", + "@loadMore": {}, + "loggedInAs": "Logged in as {userName}", + "@loggedInAs": { + "description": "gpodder.net", + "placeholders": { + "userName": {} + } + }, + "login": "Login", + "@login": { + "description": "gpodder.net login" + }, + "loginFailed": "Login failed", + "@loginFailed": {}, + "logout": "Logout", + "@logout": { + "description": "gpodder.net logout" + }, + "mark": "Marcar", + "@mark": { + "description": "In listen history page, if a episode is marked as listened." + }, + "markConfirm": "Confirmar marca", + "@markConfirm": {}, + "markConfirmContent": "Marcar todos os episódios como ouvidos?", + "@markConfirmContent": {}, + "markListened": "Marcar como ouvido", + "@markListened": {}, + "markNotListened": "Marcar não ouvidos", + "@markNotListened": {}, + "menu": "Menu", + "@menu": {}, + "menuAllPodcasts": "Todos os podcasts", + "@menuAllPodcasts": {}, + "menuMarkAllListened": "Marcar todos como ouvidos", + "@menuMarkAllListened": {}, + "menuViewRSS": "Visitar Feed RSS", + "@menuViewRSS": {}, + "menuVisitSite": "Visitar website", + "@menuVisitSite": {}, + "minsAgo": "{count, plural, zero{Agora} one{Há {count} minuto} other{Há {count} minutos}}", + "@minsAgo": {}, + "minsCount": "{count, plural, zero{0 minutos} one{{count} minuto} other{{count} minutos}}", + "@minsCount": {}, + "network": "Rede", + "@network": {}, + "newestFirst": "Mais recentes primeiro", + "@newestFirst": {}, + "newGroup": "Criar um novo grupo", + "@newGroup": {}, + "next": "Seguinte", + "@next": {}, + "noEpisodeDownload": "Ainda não há episódios descarregados", + "@noEpisodeDownload": {}, + "noEpisodeFavorite": "Ainda não há episódios coletados", + "@noEpisodeFavorite": {}, + "noEpisodeRecent": "Ainda não há episódios recebidos", + "@noEpisodeRecent": {}, + "noPodcastGroup": "Não há podcasts neste grupo", + "@noPodcastGroup": {}, + "noShownote": "Não há notas disponíveis para este episódio", + "@noShownote": { + "description": "Means this episode have no show notes." + }, + "notificaitonFatch": "Obter dados {title}", + "@notificaitonFatch": {}, + "notificationNetworkError": "A subscrição falhou, erro de rede {title}", + "@notificationNetworkError": { + "placeholders": { + "title": {} + } + }, + "notificationSetting": "Painel de notificações", + "@notificationSetting": {}, + "notificationSubscribe": "Subscrever {title}", + "@notificationSubscribe": { + "placeholders": { + "title": {} + } + }, + "notificationSubscribeExisted": "Subscrição falhou, podcast já existe {title}", + "@notificationSubscribeExisted": { + "placeholders": { + "title": {} + } + }, + "notificationSuccess": "Subscrito com sucesso {title}", + "@notificationSuccess": { + "placeholders": { + "title": {} + } + }, + "notificationUpdate": "Atualizar {title}", + "@notificationUpdate": { + "placeholders": { + "title": {} + } + }, + "notificationUpdateError": "Erro de atualização {title}", + "@notificationUpdateError": { + "placeholders": { + "title": {} + } + }, + "oldestFirst": "Mais antigos primeiro", + "@oldestFirst": {}, + "passwdRequired": "Password required", + "@passwdRequired": {}, + "password": "Password", + "@password": {}, + "pause": "Pausa", + "@pause": {}, + "play": "Reproduzir", + "@play": {}, + "playback": "Controlo da reprodução", + "@playback": {}, + "player": "Reprodutor", + "@player": {}, + "playerHeightMed": "Médio", + "@playerHeightMed": {}, + "playerHeightShort": "Baixo", + "@playerHeightShort": {}, + "playerHeightTall": "Alto", + "@playerHeightTall": {}, + "playing": "Em reprodução", + "@playing": {}, + "plugins": "Plugins", + "@plugins": {}, + "podcast": "{count, plural, zero{} one{Podcast} other{Podcasts}}", + "@podcast": {}, + "podcastSubscribed": "Podcast subscrito", + "@podcastSubscribed": {}, + "popupMenuDownloadDes": "Descarregar episódio", + "@popupMenuDownloadDes": {}, + "popupMenuLaterDes": "Adicionar episódio à lista de reprodução", + "@popupMenuLaterDes": {}, + "popupMenuLikeDes": "Adicionar episódio aos favoritos", + "@popupMenuLikeDes": {}, + "popupMenuMarkDes": "Marcar episódio como ouvido", + "@popupMenuMarkDes": {}, + "popupMenuPlayDes": "Reproduzir episódio", + "@popupMenuPlayDes": {}, + "privacyPolicy": "Política de Privacidade", + "@privacyPolicy": {}, + "published": "Publicado em {date}", + "@published": { + "placeholders": { + "date": {} + } + }, + "publishedDaily": "Publicado diariamente", + "@publishedDaily": {}, + "publishedMonthly": "Publicado mensalmente", + "@publishedMonthly": {}, + "publishedWeekly": "Publicado semanalmente", + "@publishedWeekly": { + "description": "In search podcast detail page." + }, + "publishedYearly": "Publicado anualmente", + "@publishedYearly": {}, + "recoverSubscribe": "Recuperar subscrição", + "@recoverSubscribe": { + "description": "User can recover subscribe podcast after remove it in subscribe history page." + }, + "refreshArtwork": "Atualizar capa", + "@refreshArtwork": {}, + "remove": "Remover", + "@remove": { + "description": "Remove not \"removed\". \nRemove a podcast or a group." + }, + "removeConfirm": "Confirmação de remoção", + "@removeConfirm": { + "description": "unsubscribe podcast dialog" + }, + "removedAt": "Removido em {date}", + "@removedAt": { + "description": "For example :Removed at 2020.10.10", + "placeholders": { + "date": {} + } + }, + "removePodcastDes": "Tens a certeza que pretendes cancelar a subscrição?", + "@removePodcastDes": {}, + "save": "Guardar", + "@save": {}, + "schedule": "Horário", + "@schedule": {}, + "search": "Procurar", + "@search": {}, + "searchEpisode": "Procurar episódio", + "@searchEpisode": {}, + "searchInvalidRss": "Ligação RSS inválida", + "@searchInvalidRss": {}, + "searchPodcast": "Procurar podcasts", + "@searchPodcast": {}, + "secCount": "{count, plural, zero{0 segundos} one{{count} segundo} other{{count} segundos}}", + "@secCount": {}, + "secondsAgo": "{count, plural, zero{Agora} one{Há {count} segundo} other{Há {count} segundos}}", + "@secondsAgo": {}, + "settings": "Definições", + "@settings": {}, + "settingsAccentColor": "Cor de realce", + "@settingsAccentColor": {}, + "settingsAccentColorDes": "Incluir cor de sobreposição", + "@settingsAccentColorDes": {}, + "settingsAppearance": "Aparência", + "@settingsAppearance": {}, + "settingsAppearanceDes": "Cores e temas", + "@settingsAppearanceDes": {}, + "settingsAppIntro": "Introdução da Aplicação", + "@settingsAppIntro": {}, + "settingsAudioCache": "Cache de áudio", + "@settingsAudioCache": {}, + "settingsAudioCacheDes": "Tamanho máximo da cache de áudio", + "@settingsAudioCacheDes": {}, + "settingsAutoDelete": "Eliminar downloads automaticamente após", + "@settingsAutoDelete": {}, + "settingsAutoDeleteDes": "30 dias por defeito", + "@settingsAutoDeleteDes": {}, + "settingsAutoPlayDes": "Reproduzir automaticamente o episódio seguinte", + "@settingsAutoPlayDes": {}, + "settingsBackup": "Cópia de segurança", + "@settingsBackup": {}, + "settingsBackupDes": "Cópia de segurança dos dados da aplicação", + "@settingsBackupDes": {}, + "settingsBoostVolume": "Nível de aumento de volume", + "@settingsBoostVolume": {}, + "settingsBoostVolumeDes": "Alterar nível de aumento de volume", + "@settingsBoostVolumeDes": {}, + "settingsDefaultGrid": "Vista de grelha predefinida", + "@settingsDefaultGrid": {}, + "settingsDefaultGridDownload": "Aba de downloads", + "@settingsDefaultGridDownload": {}, + "settingsDefaultGridFavorite": "Aba de favoritos", + "@settingsDefaultGridFavorite": {}, + "settingsDefaultGridPodcast": "Página de podcasts", + "@settingsDefaultGridPodcast": {}, + "settingsDefaultGridRecent": "Aba de recentes", + "@settingsDefaultGridRecent": {}, + "settingsDiscovery": "Reiniciar tutorial", + "@settingsDiscovery": { + "description": "Reset feature discovery state. User tap it and restart app, will see features tutorial again." + }, + "settingsEnableSyncing": "Ativar sincronização", + "@settingsEnableSyncing": {}, + "settingsEnableSyncingDes": "Atualizar todos os podcasts em segundo plano para obter os episódios mais recentes", + "@settingsEnableSyncingDes": {}, + "settingsExportDes": "Exportar e importar definições da aplicação", + "@settingsExportDes": {}, + "settingsFastForwardSec": "Avançar segundos", + "@settingsFastForwardSec": {}, + "settingsFastForwardSecDes": "Muda os segundos de avanço no reprodutor", + "@settingsFastForwardSecDes": {}, + "settingsFeedback": "Feedback", + "@settingsFeedback": {}, + "settingsFeedbackDes": "Erros e sugestões", + "@settingsFeedbackDes": {}, + "settingsHistory": "Histórico", + "@settingsHistory": {}, + "settingsHistoryDes": "Dados de audição", + "@settingsHistoryDes": {}, + "settingsInfo": "Informações", + "@settingsInfo": {}, + "settingsInterface": "Interface", + "@settingsInterface": {}, + "settingsLanguages": "Idiomas", + "@settingsLanguages": {}, + "settingsLanguagesDes": "Mudar idioma", + "@settingsLanguagesDes": {}, + "settingsLayout": "Esquema", + "@settingsLayout": {}, + "settingsLayoutDes": "Esquema da aplicação", + "@settingsLayoutDes": {}, + "settingsLibraries": "Bibliotecas", + "@settingsLibraries": {}, + "settingsLibrariesDes": "Bibliotecas de código aberto usados nesta aplicação", + "@settingsLibrariesDes": {}, + "settingsManageDownload": "Gerir downloads", + "@settingsManageDownload": {}, + "settingsManageDownloadDes": "Gerir arquivos de aúdio descarregados", + "@settingsManageDownloadDes": {}, + "settingsMenuAutoPlay": "Reproduzir seguinte automaticamente", + "@settingsMenuAutoPlay": {}, + "settingsNetworkCellular": "Perguntar antes de usar dados móveis", + "@settingsNetworkCellular": {}, + "settingsNetworkCellularAuto": "Descarregar automaticamente usando os dados móveis", + "@settingsNetworkCellularAuto": {}, + "settingsNetworkCellularAutoDes": "Podes configurar o descarregamento automático na página de gestão de grupos", + "@settingsNetworkCellularAutoDes": {}, + "settingsNetworkCellularDes": "Perguntar a confirmar o uso de dados móveis ao descarregar episódios", + "@settingsNetworkCellularDes": {}, + "settingsPlayDes": "Lista de reprodução e reprodutor", + "@settingsPlayDes": {}, + "settingsPlayerHeight": "Altura do reprodutor", + "@settingsPlayerHeight": {}, + "settingsPlayerHeightDes": "Mudar a altura do reprodutor a teu gosto", + "@settingsPlayerHeightDes": {}, + "settingsPopupMenu": "Menu pop-up de episódios", + "@settingsPopupMenu": {}, + "settingsPopupMenuDes": "Muda o menu pop-up de episódios", + "@settingsPopupMenuDes": {}, + "settingsPrefrence": "Preferências", + "@settingsPrefrence": {}, + "settingsRealDark": "Escuro AMOLED", + "@settingsRealDark": {}, + "settingsRealDarkDes": "Ativa caso o modo escuro não seja suficientemente escuro", + "@settingsRealDarkDes": {}, + "settingsRewindSec": "Segundos de recuo", + "@settingsRewindSec": {}, + "settingsRewindSecDes": "Muda os segundos de recuo no reprodutor", + "@settingsRewindSecDes": {}, + "settingsSpeeds": "Velocidades", + "@settingsSpeeds": { + "description": "Playback speeds setting." + }, + "settingsSpeedsDes": "Customizar as velocidades disponíveis", + "@settingsSpeedsDes": { + "description": "Playback speed setting description" + }, + "settingsSTAuto": "Ligar temporizador automaticamente", + "@settingsSTAuto": {}, + "settingsSTAutoDes": "Ligar temporizador automaticamente num horário definido", + "@settingsSTAutoDes": {}, + "settingsSTDefaultTime": "Tempo predefinido", + "@settingsSTDefaultTime": {}, + "settingsSTDefautTimeDes": "Tempo predefinido para temporizador", + "@settingsSTDefautTimeDes": {}, + "settingsSTMode": "Modo de temporizador automático", + "@settingsSTMode": {}, + "settingsStorageDes": "Gerir cache e armazenamento de downloads", + "@settingsStorageDes": {}, + "settingsSyncing": "Sincronização", + "@settingsSyncing": {}, + "settingsSyncingDes": "Atualizar podcasts em segundo plano", + "@settingsSyncingDes": {}, + "settingsTapToOpenPopupMenu": "Prime para abrir o menu pop-up", + "@settingsTapToOpenPopupMenu": {}, + "settingsTapToOpenPopupMenuDes": "Precisas manter premido para abrir a página do episódio", + "@settingsTapToOpenPopupMenuDes": {}, + "settingsTheme": "Tema", + "@settingsTheme": {}, + "settingStorage": "Armazenamento", + "@settingStorage": {}, + "settingsUpdateInterval": "Intervalo de atualização", + "@settingsUpdateInterval": {}, + "settingsUpdateIntervalDes": "24 horas predefinidas", + "@settingsUpdateIntervalDes": {}, + "share": "Partilhar", + "@share": {}, + "showNotesFonts": "Mostrar tipo de letra das notas", + "@showNotesFonts": {}, + "size": "Tamanho", + "@size": {}, + "skipSecondsAtEnd": "Saltar segundos no fim", + "@skipSecondsAtEnd": {}, + "skipSecondsAtStart": "Saltar segundos no início", + "@skipSecondsAtStart": {}, + "skipSilence": "Saltar silêncio", + "@skipSilence": { + "description": "Feature skip silence" + }, + "skipToNext": "Saltar para o próximo", + "@skipToNext": {}, + "sleepTimer": "Temporizador", + "@sleepTimer": {}, + "status": "Status", + "@status": { + "description": "gpodder.net status" + }, + "stop": "Parar", + "@stop": {}, + "subscribe": "Subscrever", + "@subscribe": {}, + "subscribeExportDes": "Exportar ficheiro OPML de todos os podcasts", + "@subscribeExportDes": {}, + "syncNow": "Sync now", + "@syncNow": {}, + "systemDefault": "Predefinido do sistema", + "@systemDefault": {}, + "timeLastPlayed": "Última vez {time}", + "@timeLastPlayed": { + "description": "Show last time stop position in player when a episode have been played.", + "placeholders": { + "time": {} + } + }, + "timeLeft": "{time} Restante", + "@timeLeft": { + "placeholders": { + "time": {} + } + }, + "to": "Para {time}", + "@to": { + "placeholders": { + "time": {} + } + }, + "toastAddPlaylist": "Adicionado à lista de reprodução", + "@toastAddPlaylist": {}, + "toastDiscovery": "Característica \"Descobrir\" ligada, por favor reinicia a aplicação", + "@toastDiscovery": { + "description": "Toast displayed when user tap Discovery Features Again in settings page." + }, + "toastFileError": "Erro no ficheiro, subscrição falhou", + "@toastFileError": {}, + "toastFileNotValid": "Ficheiro inválido", + "@toastFileNotValid": {}, + "toastHomeGroupNotSupport": "Grupo Home não é suportado", + "@toastHomeGroupNotSupport": {}, + "toastImportSettingsSuccess": "Definições importadas com sucesso", + "@toastImportSettingsSuccess": {}, + "toastOneGroup": "Seleciona pelo menos um grupo", + "@toastOneGroup": {}, + "toastPodcastRecovering": "A recuperar, espera um momento", + "@toastPodcastRecovering": { + "description": "Resubscribe removed podcast" + }, + "toastReadFile": "Ficheiro lido com sucesso", + "@toastReadFile": {}, + "toastRecoverFailed": "Recuperação do podcast falhou", + "@toastRecoverFailed": { + "description": "Resubscribe removed podast" + }, + "toastRemovePlaylist": "Episódio removido da lista de reprodução", + "@toastRemovePlaylist": {}, + "toastSettingSaved": "Definições guardadas", + "@toastSettingSaved": {}, + "toastTimeEqualEnd": "Tempo marcado é igual ao tempo de fim", + "@toastTimeEqualEnd": { + "description": "User can't choose the same time as schedule end time." + }, + "toastTimeEqualStart": "Tempo marcado é igual ao tempo de início", + "@toastTimeEqualStart": { + "description": "User can't choose the same time as schedule start time." + }, + "translators": "Tradutores", + "@translators": {}, + "understood": "Compreendido", + "@understood": {}, + "undo": "DESFAZER", + "@undo": {}, + "unlike": "Não gosto", + "@unlike": {}, + "unliked": "Episódio removido dos favoritos", + "@unliked": {}, + "updateDate": "Atualizar data", + "@updateDate": {}, + "updateEpisodesCount": "{count, plural, zero{Sem atualizações} one{{count} episódio atualizado} other{{count} episódios atualizados}}", + "@updateEpisodesCount": {}, + "updateFailed": "Atuallização falhou, erro de conexão", + "@updateFailed": {}, + "username": "Username", + "@username": {}, + "usernameRequired": "Username requeired", + "@usernameRequired": {}, + "version": "Versão: {version}", + "@version": { + "placeholders": { + "version": {} + } + } +} \ No newline at end of file diff --git a/lib/l10n/intl_zh_Hans.arb b/lib/l10n/intl_zh_Hans.arb index c1e5e1e..448d5b7 100644 --- a/lib/l10n/intl_zh_Hans.arb +++ b/lib/l10n/intl_zh_Hans.arb @@ -140,6 +140,8 @@ }, "goodNight": "晚安", "@goodNight": {}, + "gpodderLoginDes": "恭喜!您已经成功绑定 gpodder.net 账号,Tsacdop 将会自动同步您的订阅到 gpodder.net 账户。", + "@gpodderLoginDes": {}, "groupExisted": "组名已使用", "@groupExisted": { "description": "Group name validate in add group dialog. User can't add group with same name." @@ -180,12 +182,25 @@ "@hoursCount": {}, "import": "导入", "@import": {}, + "intergateWith": "绑定 {service}", + "@intergateWith": { + "description": "Integrate with", + "placeholders": { + "service": {} + } + }, "introFourthPage": "您可以长按节目打开快捷菜单。", "@introFourthPage": {}, "introSecondPage": "您可以通过搜索订阅播客,也可以直接导入OPML文件。", "@introSecondPage": {}, "introThirdPage": "您可以创建分组,上下滑动切换分组。", "@introThirdPage": {}, + "invalidName": "用户名错误", + "@invalidName": {}, + "lastUpdate": "最近更新", + "@lastUpdate": { + "description": "gpodder.net update" + }, "later": "稍后", "@later": {}, "lightMode": "明亮模式", @@ -204,6 +219,23 @@ "@listened": {}, "loadMore": "加载更多", "@loadMore": {}, + "loggedInAs": "使用{userName}登入", + "@loggedInAs": { + "description": "gpodder.net", + "placeholders": { + "userName": {} + } + }, + "login": "登入", + "@login": { + "description": "gpodder.net login" + }, + "loginFailed": "登入失败", + "@loginFailed": {}, + "logout": "注销", + "@logout": { + "description": "gpodder.net logout" + }, "mark": "标记", "@mark": { "description": "In listen history page, if a episode is marked as listened." @@ -292,6 +324,10 @@ }, "oldestFirst": "由旧到新", "@oldestFirst": {}, + "passwdRequired": "密码为空", + "@passwdRequired": {}, + "password": "密码", + "@password": {}, "pause": "暂停", "@pause": {}, "play": "播放", @@ -547,12 +583,18 @@ "@skipToNext": {}, "sleepTimer": "睡眠模式", "@sleepTimer": {}, + "status": "状态", + "@status": { + "description": "gpodder.net status" + }, "stop": "停止", "@stop": {}, "subscribe": "订阅", "@subscribe": {}, "subscribeExportDes": "导出 OPML 文件", "@subscribeExportDes": {}, + "syncNow": "立即同步", + "@syncNow": {}, "systemDefault": "系统默认", "@systemDefault": {}, "timeLastPlayed": "上次播放{time}", @@ -628,6 +670,10 @@ "@updateEpisodesCount": {}, "updateFailed": "更新失败", "@updateFailed": {}, + "username": "用户名", + "@username": {}, + "usernameRequired": "用户名为空", + "@usernameRequired": {}, "version": "版本:{version}", "@version": { "placeholders": { diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index 9d704e6..05d11a3 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -46,6 +46,13 @@ const String notificationLayoutKey = 'notificationLayoutKey'; const String showNotesFontKey = 'showNotesFontKey'; const String speedListKey = 'speedListKey'; const String searchHistoryKey = 'searchHistoryKey'; +const String gpodderApiKey = 'gpodderApiKey'; +const String gpodderAddKey = 'gpodderAddKey'; +const String gpodderRemoveKey = 'gpodderRemoveKey'; +const String gpodderSyncStatusKey = 'gpodderSyncStatusKey'; +const String gpodderSyncDateTimeKey = 'gpodderSyncDateTimeKey'; +const String gpodderRemoteAddKey = 'gpodderRemoteAddKey'; +const String gpodderRemoteRemoveKey = 'gpodderRemoteRemoveKey'; class KeyValueStorage { final String key; @@ -178,4 +185,13 @@ class KeyValueStorage { } return prefs.getDouble(key); } + + Future addList(List addList) async { + final list = await getStringList(); + await saveStringList(list..addAll(addList)); + } + + Future clearList() async { + await saveStringList([]); + } } diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 8af099c..d4d207d 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -242,7 +242,7 @@ class DBHelper { return ['', '']; } - Future delPodcastLocal(String id) async { + Future delPodcastLocal(String id) async { var dbClient = await database; await dbClient.rawDelete('DELETE FROM PodcastLocal WHERE id =?', [id]); List list = await dbClient.rawQuery( @@ -255,10 +255,10 @@ class DBHelper { } } await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]); - var _milliseconds = DateTime.now().millisecondsSinceEpoch; + var milliseconds = DateTime.now().millisecondsSinceEpoch; await dbClient.rawUpdate( """UPDATE SubscribeHistory SET remove_date = ? , status = ? WHERE id = ?""", - [_milliseconds, 1, id]); + [milliseconds, 1, id]); } Future saveHistory(PlayHistory history) async { diff --git a/lib/podcasts/podcast_group.dart b/lib/podcasts/podcast_group.dart index 4fe979c..d25c272 100644 --- a/lib/podcasts/podcast_group.dart +++ b/lib/podcasts/podcast_group.dart @@ -466,7 +466,7 @@ class __PodcastCardState extends State<_PodcastCard> splashColor: Colors.red.withAlpha(70), onPressed: () { groupList.removePodcast( - widget.podcastLocal.id); + widget.podcastLocal); Navigator.of(context).pop(); }, child: Text( diff --git a/lib/podcasts/podcast_settings.dart b/lib/podcasts/podcast_settings.dart index b80720f..926b4dc 100644 --- a/lib/podcasts/podcast_settings.dart +++ b/lib/podcasts/podcast_settings.dart @@ -390,7 +390,7 @@ class _PodcastSettingState extends State { FlatButton( splashColor: Colors.red.withAlpha(70), onPressed: () async { - await groupList.removePodcast(widget.podcastLocal.id); + await groupList.removePodcast(widget.podcastLocal); Navigator.of(context).pop(); }, child: diff --git a/lib/podcasts/podcastlist.dart b/lib/podcasts/podcastlist.dart index f03fc8a..6d48083 100644 --- a/lib/podcasts/podcastlist.dart +++ b/lib/podcasts/podcastlist.dart @@ -59,7 +59,7 @@ class _AboutPodcastState extends State { splashColor: context.accentColor.withAlpha(70), padding: EdgeInsets.all(10.0), onPressed: () { - _groupList.removePodcast(widget.podcastLocal.id); + _groupList.removePodcast(widget.podcastLocal); Navigator.of(context).pop(); }, textColor: Colors.red, diff --git a/lib/service/gpodder_api.dart b/lib/service/gpodder_api.dart new file mode 100644 index 0000000..3177ed1 --- /dev/null +++ b/lib/service/gpodder_api.dart @@ -0,0 +1,239 @@ +import 'dart:convert'; +import 'dart:developer' as developer; + +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:device_info/device_info.dart'; +import 'package:dio/dio.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; + +import '../local_storage/key_value_storage.dart'; +import '../local_storage/sqflite_localpodcast.dart'; + +enum GpodderSyncStatus { none, success, fail, authError } + +class Gpodder { + final _dio = Dio(BaseOptions( + connectTimeout: 30000, + receiveTimeout: 90000, + sendTimeout: 90000, + )); + final _storage = KeyValueStorage(gpodderApiKey); + final _addStorage = KeyValueStorage(gpodderAddKey); + final _removeStorage = KeyValueStorage(gpodderRemoveKey); + final _remoteAddStorage = KeyValueStorage(gpodderRemoteAddKey); + final _remoteRemoveStorage = KeyValueStorage(gpodderRemoteRemoveKey); + final _dateTimeStorage = KeyValueStorage(gpodderSyncDateTimeKey); + final _statusStorage = KeyValueStorage(gpodderSyncStatusKey); + + final _baseUrl = "https://gpodder.net"; + + Future _initDio() async { + final dir = await getApplicationDocumentsDirectory(); + var cookieJar = PersistCookieJar(dir: "${dir.path}/.cookies/"); + _dio.interceptors.add(CookieManager(cookieJar)); + } + + Future login({String username, String password}) async { + final dir = await getApplicationDocumentsDirectory(); + var cookieJar = PersistCookieJar(dir: "${dir.path}/.cookies/"); + cookieJar.delete(Uri.parse(_baseUrl)); + _dio.interceptors.add(CookieManager(cookieJar)); + final basicAuth = + 'Basic ${base64Encode(utf8.encode('$username:$password'))}'; + var status; + Response response; + try { + response = await _dio.post('$_baseUrl/api/2/auth/$username/login.json', + options: + Options(headers: {'authorization': basicAuth})); + status = response.statusCode; + } catch (e) { + developer.log(e.toString(), name: 'gpoderr login error'); + return 0; + } + return status; + } + + Future logout() async { + final loginInfo = await _storage.getStringList(); + final username = loginInfo[0]; + await _initDio(); + var status; + try { + var response = await _dio.post( + '$_baseUrl/api/2/auth/$username/logout.json', + ); + status = response.statusCode; + } catch (e) { + developer.log(e.toString(), name: 'gpoderr logout error'); + if (status == 400) { + await _initService(); + } + return 0; + } + if (status == 200) { + await _initService(); + } + return status; + } + + Future _initService() async { + final dir = await getApplicationDocumentsDirectory(); + var cookieJar = PersistCookieJar(dir: "${dir.path}/.cookies/"); + cookieJar.delete(Uri.parse(_baseUrl)); + await _storage.clearList(); + await _addStorage.clearList(); + await _remoteAddStorage.clearList(); + await _removeStorage.clearList(); + await _remoteAddStorage.clearList(); + await _statusStorage.saveInt(0); + await _dateTimeStorage.saveInt(0); + } + + Future checkLogin(String username) async { + await _initDio(); + var response = await _dio.post( + '$_baseUrl/api/2/auth/$username/login.json', + ); + final status = response.statusCode; + return status; + } + + Future updateDevice(String username) async { + await _initDio(); + final deviceId = Uuid().v1(); + final androidInfo = await DeviceInfoPlugin().androidInfo; + var status = 0; + try { + var response = await _dio + .post("$_baseUrl/api/2/devices/$username/$deviceId.json", data: { + "caption": "Tsacdop on ${androidInfo.model}", + "type": "mobile" + }); + status = response.statusCode; + } catch (e) { + developer.log(e.toString(), name: 'gpodder update device error'); + return 0; + } + if (status == 200) { + await _storage.saveStringList([username, deviceId]); + } + return status; + } + + Future getAllPodcast() async { + final loginInfo = await _storage.getStringList(); + final username = loginInfo[0]; + Response response; + await _initDio(); + try { + response = await _dio.get( + '$_baseUrl/subscriptions/$username.opml', + ); + } catch (e) { + developer.log(e.toString(), name: 'gpodder update podcasts error'); + return ''; + } + return response.data; + } + + Future uploadSubscriptions() async { + final syncDataTime = DateTime.now().millisecondsSinceEpoch; + await _dateTimeStorage.saveInt(syncDataTime); + final loginInfo = await _storage.getStringList(); + final username = loginInfo[0]; + final deviceId = loginInfo[1]; + await _initDio(); + final dbHelper = DBHelper(); + final podcasts = await dbHelper.getPodcastLocalAll(); + var subscriptions = ''; + for (var podcast in podcasts) { + subscriptions += '${podcast.rssUrl}\n'; + } + var status; + try { + final response = await _dio.put( + '$_baseUrl/subscriptions/$username/$deviceId.txt', + data: subscriptions); + status = response.statusCode; + } catch (e) { + developer.log(e.toString(), name: 'gpodder update podcasts error'); + return 0; + } + return status; + } + + Future getChanges() async { + final loginInfo = await _storage.getStringList(); + final username = loginInfo[0]; + final deviceId = loginInfo[1]; + final syncDataTime = DateTime.now().millisecondsSinceEpoch; + await _dateTimeStorage.saveInt(syncDataTime); + final timeStamp = loginInfo.length == 3 ? int.parse(loginInfo[2]) : 0; + var status; + Response response; + await _initDio(); + try { + response = await _dio.get( + "$_baseUrl/api/2/subscriptions/$username/$deviceId.json", + queryParameters: {'since': timeStamp}); + status = response.statusCode; + } catch (e) { + developer.log(e.toString(), name: 'gpodder update podcasts error'); + if (status == 401) { + _statusStorage.saveInt(3); + } else { + _statusStorage.saveInt(2); + } + return 0; + } + if (status == 200) { + Map changes = jsonDecode(response.toString()); + final timeStamp = changes['timestamp']; + final addList = changes['add'].cast(); + final removeList = changes['remove'].cast(); + print(removeList); + await _storage.saveStringList([username, deviceId, timeStamp.toString()]); + await _remoteAddStorage.addList(addList); + await _remoteRemoveStorage.addList(removeList); + } + return status; + } + + Future updateChange() async { + final loginInfo = await _storage.getStringList(); + final addList = await _addStorage.getStringList(); + final removeList = await _removeStorage.getStringList(); + final username = loginInfo[0]; + final deviceId = loginInfo[1]; + await _initDio(); + var status; + Response response; + try { + response = await _dio.post( + '$_baseUrl/api/2/subscriptions/$username/$deviceId.json', + data: {'add': addList, 'remove': removeList}); + status = response.statusCode; + } catch (e) { + if (status == 401) { + _statusStorage.saveInt(3); + } else { + _statusStorage.saveInt(2); + } + developer.log(e.toString(), name: 'gpodder update podcasts error'); + return 0; + } + if (status == 200) { + await _addStorage.clearList(); + await _removeStorage.clearList(); + await _statusStorage.saveInt(1); + Map changes = jsonDecode(response.toString()); + final timeStamp = changes['timestamp'] as int; + await _storage + .saveStringList([username, deviceId, (timeStamp + 1).toString()]); + } + return status; + } +} diff --git a/lib/service/ompl_build.dart b/lib/service/opml_build.dart similarity index 93% rename from lib/service/ompl_build.dart rename to lib/service/opml_build.dart index f13e22c..56e02cb 100644 --- a/lib/service/ompl_build.dart +++ b/lib/service/opml_build.dart @@ -1,5 +1,4 @@ import 'dart:developer' as developer; -import 'dart:io'; import 'package:xml/xml.dart' as xml; import '../state/podcast_group.dart'; @@ -21,9 +20,9 @@ class OmplOutline { class PodcastsBackup { ///Group list for backup. final List groups; - PodcastsBackup(this.groups) : assert(groups.length > 0); + PodcastsBackup(this.groups) : assert(groups.isNotEmpty); - omplBuilder() { + xml.XmlNode omplBuilder() { var builder = xml.XmlBuilder(); builder.processing('xml', 'version="1.0" encoding="UTF-8"'); builder.element('ompl', nest: () { @@ -55,9 +54,9 @@ class PodcastsBackup { return builder.build(); } - static parseOMPL(File file) { + static parseOMPL(String opml) { var data = >{}; - var opml = file.readAsStringSync(); + // var opml = file.readAsStringSync(); var content = xml.XmlDocument.parse(opml); var title = content.findAllElements('head').first.findElements('title').first.text; diff --git a/lib/service/api_search.dart b/lib/service/search_api.dart similarity index 71% rename from lib/service/api_search.dart rename to lib/service/search_api.dart index 04ea042..58c559e 100644 --- a/lib/service/api_search.dart +++ b/lib/service/search_api.dart @@ -3,11 +3,11 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import '../.env.dart'; -import '../type/search_top_podcast.dart'; -import '../type/searchepisodes.dart'; -import '../type/searchpodcast.dart'; +import '../type/search_api/search_top_podcast.dart'; +import '../type/search_api/searchepisodes.dart'; +import '../type/search_api/searchpodcast.dart'; -class SearchEngine { +class ListenNotesSearch { final apiKey = environment['apiKey']; Future> searchPodcasts( {String searchText, int nextOffset}) async { @@ -51,3 +51,16 @@ class SearchEngine { return searchResult; } } + +class ItunesSearch { + Future> searchPodcasts( + {String searchText, int limit}) async { + var url = "https://itunes.apple.com/search/search?q=" + "${Uri.encodeComponent(searchText)}${"&media=podcast&entity=podcast&limit=$limit"}"; + var response = await Dio() + .get(url, options: Options(headers: {'Accept': "application/json"})); + Map searchResultMap = jsonDecode(response.toString()); + var searchResult = SearchPodcast.fromJson(searchResultMap); + return searchResult; + } +} diff --git a/lib/settings/data_backup.dart b/lib/settings/data_backup.dart index 15fc2ba..8d92d72 100644 --- a/lib/settings/data_backup.dart +++ b/lib/settings/data_backup.dart @@ -3,17 +3,23 @@ import 'dart:developer' as developer; import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:line_icons/line_icons.dart'; -import 'package:path/path.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:tsacdop/util/custom_widget.dart'; +import 'package:tuple/tuple.dart'; import 'package:wc_flutter_share/wc_flutter_share.dart'; -import '../service/ompl_build.dart'; +import '../local_storage/key_value_storage.dart'; +import '../local_storage/sqflite_localpodcast.dart'; +import '../service/gpodder_api.dart'; +import '../service/opml_build.dart'; import '../state/podcast_group.dart'; import '../state/setting_state.dart'; import '../type/settings_backup.dart'; @@ -25,17 +31,20 @@ class DataBackup extends StatefulWidget { } class _DataBackupState extends State { + final _gpodder = Gpodder(); + var _syncing = false; + Future _exportOmpl(BuildContext context) async { var groups = context.read().groups; - var ompl = PodcastsBackup(groups).omplBuilder(); + var opml = PodcastsBackup(groups).omplBuilder(); var tempdir = await getTemporaryDirectory(); var now = DateTime.now(); var datePlus = now.year.toString() + now.month.toString() + now.day.toString() + now.second.toString(); - var file = File(join(tempdir.path, 'tsacdop_ompl_$datePlus.xml')); - await file.writeAsString(ompl.toString()); + var file = File(path.join(tempdir.path, 'tsacdop_ompl_$datePlus.xml')); + await file.writeAsString(opml.toXmlString()); return file; } @@ -63,12 +72,12 @@ class _DataBackupState extends State { now.month.toString() + now.day.toString() + now.second.toString(); - var file = File(join(tempdir.path, 'tsacdop_settings_$datePlus.json')); + var file = File(path.join(tempdir.path, 'tsacdop_settings_$datePlus.json')); await file.writeAsString(jsonEncode(json)); return file; } - Future _importSetting(String path, BuildContext context) async { + Future _importSetting(String path, BuildContext context) async { final s = context.s; var settings = context.read(); var file = File(path); @@ -89,23 +98,83 @@ class _DataBackupState extends State { } } + Widget _syncStauts(int index) { + switch (index) { + case 1: + return Text('Success', style: TextStyle(color: Colors.green)); + break; + case 2: + return Text('Failed', style: TextStyle(color: Colors.red)); + break; + case 3: + return Text('Unauthorized', style: TextStyle(color: Colors.red)); + break; + default: + return Text('Unknown'); + break; + } + } + void _getFilePath(BuildContext context) async { final s = context.s; try { - var filePath = await FilePicker.getFilePath(type: FileType.any); - if (filePath == '') { + var filePickResult = + await FilePicker.platform.pickFiles(type: FileType.any); + if (filePickResult == null) { return; } Fluttertoast.showToast( msg: s.toastReadFile, gravity: ToastGravity.BOTTOM, ); + final filePath = filePickResult.files.first.path; _importSetting(filePath, context); } on PlatformException catch (e) { developer.log(e.toString(), name: 'Get file path'); } } + Future _logout() async { + await _gpodder.logout(); + final subscribeWorker = context.read(); + subscribeWorker.cancelWork(); + Fluttertoast.showToast( + msg: 'Logout successfully', + gravity: ToastGravity.BOTTOM, + ); + if (mounted) setState(() {}); + } + + Future> _getLoginInfo() async { + final storage = KeyValueStorage(gpodderApiKey); + return await storage.getStringList(); + } + + Future _syncNow() async { + setState(() { + _syncing = true; + }); + final gpodder = Gpodder(); + final status = await gpodder.getChanges(); + + if (status == 200) { + final groupList = context.read(); + await gpodder.updateChange(); + await groupList.gpodderSyncNow(); + } + if (mounted) { + setState(() { + _syncing = false; + }); + } + } + + Future> _getSyncStatus() async { + final syncDateTime = await KeyValueStorage(gpodderSyncDateTimeKey).getInt(); + final statusIndex = await KeyValueStorage(gpodderSyncStatusKey).getInt(); + return Tuple2(syncDateTime, statusIndex); + } + @override Widget build(BuildContext context) { final s = context.s; @@ -129,13 +198,136 @@ class _DataBackupState extends State { Padding( padding: EdgeInsets.all(10.0), ), + FutureBuilder>( + future: _getLoginInfo(), + initialData: [], + builder: (context, snapshot) { + final loginInfo = snapshot.data; + return Container( + height: 160, + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Stack( + children: [ + Hero( + tag: 'gpodder.net', + child: CircleAvatar( + minRadius: 40, + backgroundColor: context.primaryColor, + child: SizedBox( + height: 60, + width: 60, + child: Image.asset('assets/gpodder.png')), + ), + ), + if (_syncing) + Positioned( + left: context.width / 2 - 40, + child: SizedBox( + height: 80, + width: 80, + child: CircularProgressIndicator( + strokeWidth: 1, + ), + ), + ), + if (_syncing) + Positioned( + bottom: 39, + left: context.width / 2 - 12, + child: _OpenEye()), + if (_syncing) + Positioned( + bottom: 39, + left: context.width / 2 + 3, + child: _OpenEye()), + ], + ), + Text( + loginInfo.isEmpty + ? s.intergateWith('gpodder.net') + : s.loggedInAs(loginInfo.first), + style: TextStyle(color: Colors.purple[700])), + ButtonTheme( + height: 32, + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.purple[700])), + highlightedBorderColor: Colors.purple[700], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LineIcons.user, + color: Colors.purple[700], + size: context.textTheme.headline6.fontSize, + ), + SizedBox(width: 10), + Text(loginInfo.isEmpty ? s.login : s.logout, + style: + TextStyle(color: Colors.purple[700])), + ], + ), + onPressed: () { + if (loginInfo.isEmpty) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _LoginGpodder(), + fullscreenDialog: true)); + } else { + _logout(); + } + }, + ), + ), + ], + ), + ); + }), + FutureBuilder>( + future: _getLoginInfo(), + initialData: [], + builder: (context, snapshot) { + final loginInfo = snapshot.data; + if (loginInfo.isNotEmpty) { + return ListTile( + contentPadding: + const EdgeInsets.only(left: 70.0, right: 20), + onTap: _syncNow, + title: Text(s.syncNow), + subtitle: FutureBuilder>( + future: _getSyncStatus(), + initialData: Tuple2(0, 0), + builder: (context, snapshot) { + final dateTime = snapshot.data.item1; + final status = snapshot.data.item2; + return Wrap( + children: [ + Text( + '${s.lastUpdate}: ${dateTime.toDate(context)}'), + SizedBox(width: 8), + Text('${s.status}: '), + _syncStauts(status), + ], + ); + }), + ); + } + return Center(); + }), + Divider(height: 1), Container( height: 30.0, - padding: EdgeInsets.symmetric(horizontal: 70), + padding: EdgeInsets.fromLTRB(70, 0, 70, 0), alignment: Alignment.centerLeft, child: Text(s.subscribe, style: context.textTheme.bodyText1 - .copyWith(color: Theme.of(context).accentColor)), + .copyWith(color: context.accentColor)), ), Padding( padding: @@ -147,49 +339,55 @@ class _DataBackupState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.green[700])), - highlightedBorderColor: Colors.green[700], - child: Row( - children: [ - Icon( - LineIcons.save, - color: Colors.green[700], - size: context.textTheme.headline6.fontSize, - ), - SizedBox(width: 10), - Text(s.save, - style: TextStyle(color: Colors.green[700])), - ], - ), - onPressed: () async { - var file = await _exportOmpl(context); - await _saveFile(file); - }), + ButtonTheme( + height: 32, + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.green[700])), + highlightedBorderColor: Colors.green[700], + child: Row( + children: [ + Icon( + LineIcons.save, + color: Colors.green[700], + size: context.textTheme.headline6.fontSize, + ), + SizedBox(width: 10), + Text(s.save, + style: TextStyle(color: Colors.green[700])), + ], + ), + onPressed: () async { + var file = await _exportOmpl(context); + await _saveFile(file); + }), + ), SizedBox(width: 10), - OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.blue[700])), - highlightedBorderColor: Colors.blue[700], - child: Row( - children: [ - Icon( - Icons.share, - size: context.textTheme.headline6.fontSize, - color: Colors.blue[700], - ), - SizedBox(width: 10), - Text(s.share, - style: TextStyle(color: Colors.blue[700])), - ], - ), - onPressed: () async { - var file = await _exportOmpl(context); - await _shareFile(file); - }) + ButtonTheme( + height: 32, + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.blue[700])), + highlightedBorderColor: Colors.blue[700], + child: Row( + children: [ + Icon( + Icons.share, + size: context.textTheme.headline6.fontSize, + color: Colors.blue[700], + ), + SizedBox(width: 10), + Text(s.share, + style: TextStyle(color: Colors.blue[700])), + ], + ), + onPressed: () async { + var file = await _exportOmpl(context); + await _shareFile(file); + }), + ) ], ), ), @@ -210,81 +408,503 @@ class _DataBackupState extends State { Padding( padding: EdgeInsets.only(left: 70.0, right: 10), child: Wrap(children: [ - OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.green[700])), - highlightedBorderColor: Colors.green[700], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - LineIcons.save, - color: Colors.green[700], - size: context.textTheme.headline6.fontSize, - ), - SizedBox(width: 10), - Text(s.save, - style: TextStyle(color: Colors.green[700])), - ], - ), - onPressed: () async { - var file = await _exportSetting(context); - await _saveFile(file); - }), - SizedBox(width: 10), - OutlineButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.blue[700])), - highlightedBorderColor: Colors.blue[700], - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.share, - size: context.textTheme.headline6.fontSize, - color: Colors.blue[700], - ), - SizedBox(width: 10), - Text(s.share, - style: TextStyle(color: Colors.blue[700])), - ], - ), - onPressed: () async { - var file = await _exportSetting(context); - await _shareFile(file); - }), - SizedBox(width: 10), ButtonTheme( height: 32, child: OutlineButton( - highlightedBorderColor: Colors.red[700], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(100.0), - side: BorderSide(color: Colors.red[700])), + side: BorderSide(color: Colors.green[700])), + highlightedBorderColor: Colors.green[700], child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( - LineIcons.paperclip_solid, + LineIcons.save, + color: Colors.green[700], size: context.textTheme.headline6.fontSize, - color: Colors.red[700], ), SizedBox(width: 10), - Text(s.import, - style: TextStyle(color: Colors.red[700])), + Text(s.save, + style: TextStyle(color: Colors.green[700])), ], ), - onPressed: () { - _getFilePath(context); + onPressed: () async { + var file = await _exportSetting(context); + await _saveFile(file); }), ), + SizedBox(width: 10), + ButtonTheme( + height: 32, + child: OutlineButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.blue[700])), + highlightedBorderColor: Colors.blue[700], + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.share, + size: context.textTheme.headline6.fontSize, + color: Colors.blue[700], + ), + SizedBox(width: 10), + Text(s.share, + style: TextStyle(color: Colors.blue[700])), + ], + ), + onPressed: () async { + var file = await _exportSetting(context); + await _shareFile(file); + }), + ), + SizedBox(width: 10), + ButtonTheme( + height: 32, + child: OutlineButton( + highlightedBorderColor: Colors.red[700], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(100.0), + side: BorderSide(color: Colors.red[700])), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LineIcons.paperclip_solid, + size: context.textTheme.headline6.fontSize, + color: Colors.red[700], + ), + SizedBox(width: 10), + Text(s.import, + style: TextStyle(color: Colors.red[700])), + ], + ), + onPressed: () { + _getFilePath(context); + }, + ), + ), ]), ), - Divider() + Divider(height: 1) ], )), ); } } + +class _OpenEye extends StatefulWidget { + _OpenEye({Key key}) : super(key: key); + + @override + __OpenEyeState createState() => __OpenEyeState(); +} + +class __OpenEyeState extends State<_OpenEye> + with SingleTickerProviderStateMixin { + double _radius = 0.0; + Animation _animation; + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = + AnimationController(vsync: this, duration: Duration(seconds: 1)); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) { + setState(() { + _radius = _animation.value; + }); + } + }); + _controller.forward(); + _controller.addStatusListener((status) async { + if (status == AnimationStatus.completed) { + await Future.delayed(Duration(milliseconds: 400)); + _controller.reverse(); + } else if (status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DotIndicator(radius: 8 * _radius + 0.5, color: Colors.white); + } +} + +enum LoginStatus { none, error, start, syncing, complete } + +class _LoginGpodder extends StatefulWidget { + _LoginGpodder({Key key}) : super(key: key); + + @override + __LoginGpodderState createState() => __LoginGpodderState(); +} + +class __LoginGpodderState extends State<_LoginGpodder> { + var _username = ''; + var _password = ''; + LoginStatus _loginStatus; + + @override + void initState() { + _loginStatus = LoginStatus.none; + super.initState(); + } + + final GlobalKey> _passwordFieldKey = + GlobalKey>(); + final GlobalKey _formKey = GlobalKey(); + final _gpodder = Gpodder(); + + Future _handleLogin() async { + setState(() => _loginStatus = LoginStatus.start); + final form = _formKey.currentState; + if (form.validate()) { + form.save(); + final status = + await _gpodder.login(username: _username, password: _password); + if (status == 200) { + final updateDevice = await _gpodder.updateDevice(_username); + if (updateDevice == 200) { + if (mounted) { + setState(() { + _loginStatus = LoginStatus.syncing; + }); + } + final uploadStatus = await _gpodder.uploadSubscriptions(); + await _getSubscriptions(_gpodder); + if (uploadStatus == 200) { + if (mounted) { + setState(() { + _loginStatus = LoginStatus.complete; + }); + } + } + } else { + if (mounted) setState(() => _loginStatus = LoginStatus.error); + Fluttertoast.showToast( + msg: context.s.loginFailed, + gravity: ToastGravity.BOTTOM, + ); + } + } else { + if (mounted) setState(() => _loginStatus = LoginStatus.error); + Fluttertoast.showToast( + msg: context.s.loginFailed, + gravity: ToastGravity.BOTTOM, + ); + } + } else { + if (mounted) setState(() => _loginStatus = LoginStatus.none); + } + } + + Future _getSubscriptions(Gpodder gpodder) async { + var subscribeWorker = context.read(); + var rssExp = RegExp(r'^(https?):\/\/(.*)'); + final opml = await gpodder.getAllPodcast(); + if (opml != '') { + Map> data = PodcastsBackup.parseOMPL(opml); + for (var entry in data.entries) { + var list = entry.value.reversed; + for (var rss in list) { + var rssLink = rssExp.stringMatch(rss.xmlUrl); + if (rssLink != null) { + final dbHelper = DBHelper(); + final exist = dbHelper.checkPodcast(rssLink); + if (exist == '') { + var item = SubscribeItem(rssLink, rss.text, group: 'Home'); + await subscribeWorker.setSubscribeItem(item, syncGpodder: false); + await Future.delayed(Duration(milliseconds: 200)); + } + } + } + } + } + await subscribeWorker.cancelWork(); + subscribeWorker.setWorkManager(4); + } + + String _validateName(String value) { + if (value.isEmpty) { + return context.s.usernameRequired; + } + final nameExp = RegExp(r'^[A-Za-z ]+$'); + if (!nameExp.hasMatch(value)) { + return context.s.invalidName; + } + return null; + } + + String _validatePassword(String value) { + final passwordField = _passwordFieldKey.currentState; + if (passwordField.value == null || passwordField.value.isEmpty) { + return context.s.passwdRequired; + } + return null; + } + + Widget _loginStatusButton() { + switch (_loginStatus) { + case LoginStatus.none: + return Text( + context.s.login, + style: TextStyle(color: Colors.white), + ); + break; + case LoginStatus.syncing: + return Text( + context.s.settingsSyncing, + style: TextStyle(color: Colors.white), + ); + break; + case LoginStatus.start: + return SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ); + default: + return Text( + context.s.login, + style: TextStyle(color: Colors.white), + ); + break; + } + } + + @override + Widget build(BuildContext context) { + final s = context.s; + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.dark, + systemNavigationBarColor: Theme.of(context).primaryColor, + systemNavigationBarIconBrightness: + Theme.of(context).accentColorBrightness, + ), + child: Scaffold( + resizeToAvoidBottomInset: true, + body: SafeArea( + top: false, + child: CustomScrollView( + slivers: [ + SliverAppBar( + brightness: Brightness.dark, + iconTheme: IconThemeData( + color: Colors.white, + ), + elevation: 0, + backgroundColor: context.accentColor, + expandedHeight: 200, + flexibleSpace: Container( + height: 200, + width: double.infinity, + color: context.accentColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Hero( + tag: 'gpodder.net', + child: CircleAvatar( + minRadius: 50, + backgroundColor: + context.primaryColor.withOpacity(0.3), + child: SizedBox( + height: 80, + width: 80, + child: Image.asset('assets/gpodder.png')), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(s.intergateWith('gpodder.net'), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold)), + ), + ], + ), + ), + ), + _loginStatus == LoginStatus.complete + ? SliverList( + delegate: SliverChildListDelegate([ + Padding( + padding: const EdgeInsets.fromLTRB(40.0, 50, 40, 100), + child: Text( + s.gpodderLoginDes, + textAlign: TextAlign.center, + ), + ), + Center( + child: OutlineButton( + onPressed: () { + Navigator.of(context).pop(); + }, + highlightedBorderColor: context.accentColor, + child: Text(s.back)), + ) + ]), + ) + : Form( + key: _formKey, + autovalidate: false, + child: AutofillGroup( + child: SliverList( + delegate: SliverChildListDelegate( + [ + Padding( + padding: + const EdgeInsets.fromLTRB(40, 20, 40, 10), + child: TextFormField( + decoration: InputDecoration( + labelStyle: + TextStyle(color: context.accentColor), + focusColor: context.accentColor, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: context.accentColor, + width: 2)), + border: OutlineInputBorder( + borderSide: BorderSide( + color: context.accentColor)), + labelText: s.username, + ), + maxLines: 1, + autofocus: true, + validator: _validateName, + autofillHints: [AutofillHints.username], + onSaved: (value) { + setState(() => _username = value); + }, + ), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(40, 10, 40, 20), + child: PasswordField( + fieldKey: _passwordFieldKey, + labelText: s.password, + validator: _validatePassword, + onSaved: (value) { + setState(() { + _password = value; + }); + }, + ), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(40, 10, 40, 20), + child: InkWell( + onTap: () { + _handleLogin(); + }, + borderRadius: BorderRadius.circular(5.0), + child: Container( + height: 40, + width: 150, + decoration: BoxDecoration( + color: context.accentColor, + borderRadius: + BorderRadius.circular(5.0)), + child: Center(child: _loginStatusButton()), + ), + ), + ), + SizedBox( + height: + MediaQuery.of(context).viewInsets.bottom, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class PasswordField extends StatefulWidget { + const PasswordField({ + this.fieldKey, + this.hintText, + this.labelText, + this.helperText, + this.onSaved, + this.validator, + this.onFieldSubmitted, + }); + + final Key fieldKey; + final String hintText; + final String labelText; + final String helperText; + final FormFieldSetter onSaved; + final FormFieldValidator validator; + final ValueChanged onFieldSubmitted; + + @override + _PasswordFieldState createState() => _PasswordFieldState(); +} + +class _PasswordFieldState extends State { + bool _obscureText = true; + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.fieldKey, + obscureText: _obscureText, + autofillHints: [AutofillHints.password], + maxLength: 100, + onSaved: widget.onSaved, + validator: widget.validator, + onFieldSubmitted: widget.onFieldSubmitted, + decoration: InputDecoration( + hintStyle: TextStyle(color: context.accentColor), + labelStyle: TextStyle(color: context.accentColor), + border: OutlineInputBorder( + borderSide: BorderSide(color: context.accentColor)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: context.accentColor, width: 2)), + hintText: widget.hintText, + labelText: widget.labelText, + helperText: widget.helperText, + suffixIcon: GestureDetector( + dragStartBehavior: DragStartBehavior.down, + onTap: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + child: Icon( + _obscureText ? Icons.visibility : Icons.visibility_off, + color: context.accentColor, + semanticLabel: _obscureText ? 'Show' : 'Hide', + ), + ), + ), + ); + } +} diff --git a/lib/settings/history.dart b/lib/settings/history.dart index 3b497d2..88d659c 100644 --- a/lib/settings/history.dart +++ b/lib/settings/history.dart @@ -13,7 +13,7 @@ import 'package:webfeed/webfeed.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../state/podcast_group.dart'; import '../type/play_histroy.dart'; -import '../type/searchpodcast.dart'; +import '../type/search_api/searchpodcast.dart'; import '../type/sub_history.dart'; import '../util/extension_helper.dart'; @@ -128,7 +128,7 @@ class _PlayedHistoryState extends State Theme.of(context).accentColorBrightness, ), child: Scaffold( - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, body: SafeArea( child: NestedScrollView( headerSliverBuilder: (context, innerBoxScrolled) { diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 333aebf..8b7dc2d 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -62,6 +62,7 @@ class _SettingsState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(10.0), @@ -71,9 +72,7 @@ class _SettingsState extends State { padding: EdgeInsets.symmetric(horizontal: 70), alignment: Alignment.centerLeft, child: Text(s.settingsPrefrence, - style: Theme.of(context) - .textTheme - .bodyText1 + style: context.textTheme.bodyText1 .copyWith(color: context.accentColor)), ), ListTile( diff --git a/lib/settings/syncing.dart b/lib/settings/syncing.dart index 5a3ccdf..09fefba 100644 --- a/lib/settings/syncing.dart +++ b/lib/settings/syncing.dart @@ -26,97 +26,73 @@ class SyncingSetting extends StatelessWidget { backgroundColor: Theme.of(context).primaryColor, ), body: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Selector>( - selector: (_, settings) => - Tuple2(settings.autoUpdate, settings.updateInterval), - builder: (_, data, __) => Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.all(10.0), - ), - Container( - height: 30.0, - padding: const EdgeInsets.symmetric(horizontal: 70), - alignment: Alignment.centerLeft, - child: Text(s.settingsSyncing, - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Theme.of(context).accentColor)), - ), - Padding( - padding: const EdgeInsets.all(5.0), - ), - ListView( - physics: const BouncingScrollPhysics(), - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - ListTile( - onTap: () { - if (settings.autoUpdate) { - settings.autoUpdate = false; - settings.cancelWork(); - } else { - settings.autoUpdate = true; - settings.setWorkManager(data.item2); - } - }, - contentPadding: const EdgeInsets.only( - left: 70.0, right: 20, bottom: 10), - title: Text(s.settingsEnableSyncing), - subtitle: Text(s.settingsEnableSyncingDes), - trailing: Transform.scale( - scale: 0.9, - child: Switch( - value: data.item1, - onChanged: (boo) async { - settings.autoUpdate = boo; - if (boo) { - settings.setWorkManager(data.item2); - } else { - settings.cancelWork(); - } - }), - ), - ), - ListTile( - contentPadding: - const EdgeInsets.only(left: 70.0, right: 20), - title: Text(s.settingsUpdateInterval), - subtitle: Text(s.settingsUpdateIntervalDes), - trailing: MyDropdownButton( - hint: Text(s.hoursCount(data.item2)), - underline: Center(), - elevation: 1, - displayItemCount: 5, - value: data.item2, - onChanged: data.item1 - ? (value) async { - await settings.cancelWork(); - settings.setWorkManager(value); - } - : null, - items: [1, 2, 4, 8, 24, 48] - .map>((e) { - return DropdownMenuItem( - value: e, child: Text(s.hoursCount(e))); - }).toList()), - ), - Divider(height: 1), - ], - ), - ], + child: Selector>( + selector: (_, settings) => + Tuple2(settings.autoUpdate, settings.updateInterval), + builder: (_, data, __) => Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(70, 20, 70, 10), + child: Text(s.settingsSyncing, + style: context.textTheme.bodyText1 + .copyWith(color: context.accentColor)), ), - ), - ], + ListTile( + onTap: () { + if (settings.autoUpdate) { + settings.autoUpdate = false; + settings.cancelWork(); + } else { + settings.autoUpdate = true; + settings.setWorkManager(data.item2); + } + }, + contentPadding: + const EdgeInsets.only(left: 70.0, right: 20, bottom: 10), + title: Text(s.settingsEnableSyncing), + subtitle: Text(s.settingsEnableSyncingDes), + trailing: Transform.scale( + scale: 0.9, + child: Switch( + value: data.item1, + onChanged: (boo) async { + settings.autoUpdate = boo; + if (boo) { + settings.setWorkManager(data.item2); + } else { + settings.cancelWork(); + } + }), + ), + ), + ListTile( + contentPadding: const EdgeInsets.only(left: 70.0, right: 20), + title: Text(s.settingsUpdateInterval), + subtitle: Text(s.settingsUpdateIntervalDes), + trailing: MyDropdownButton( + hint: Text(s.hoursCount(data.item2)), + underline: Center(), + elevation: 1, + displayItemCount: 5, + value: data.item2, + onChanged: data.item1 + ? (value) async { + await settings.cancelWork(); + settings.setWorkManager(value); + } + : null, + items: [1, 2, 4, 8, 24, 48] + .map>((e) { + return DropdownMenuItem( + value: e, child: Text(s.hoursCount(e))); + }).toList()), + ), + Divider(height: 1), + ], + ), ), ), ), diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index cc4be0f..2ad131c 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -185,10 +185,8 @@ class ThemeSetting extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 70), alignment: Alignment.centerLeft, child: Text(s.fontStyle, - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Theme.of(context).accentColor)), + style: context.textTheme.bodyText1 + .copyWith(color: context.accentColor)), ), Selector( selector: (_, setting) => setting.showNotesFontIndex, diff --git a/lib/state/podcast_group.dart b/lib/state/podcast_group.dart index b78dadc..39de481 100644 --- a/lib/state/podcast_group.dart +++ b/lib/state/podcast_group.dart @@ -3,23 +3,39 @@ import 'dart:developer' as developer; import 'dart:io'; import 'dart:isolate'; import 'dart:math' as math; +import 'dart:ui'; import 'package:color_thief_flutter/color_thief_flutter.dart'; import 'package:dio/dio.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_isolate/flutter_isolate.dart'; import 'package:image/image.dart' as img; import 'package:path_provider/path_provider.dart'; -import 'package:webfeed/webfeed.dart'; import 'package:uuid/uuid.dart'; -import 'package:equatable/equatable.dart'; +import 'package:webfeed/webfeed.dart'; +import 'package:workmanager/workmanager.dart'; import '../local_storage/key_value_storage.dart'; import '../local_storage/sqflite_localpodcast.dart'; +import '../service/gpodder_api.dart'; import '../type/fireside_data.dart'; import '../type/podcastlocal.dart'; +void callbackDispatcher() { + if (Platform.isAndroid) { + Workmanager.executeTask((task, inputData) async { + final gpodder = Gpodder(); + final status = await gpodder.getChanges(); + if (status == 200) { + await gpodder.updateChange(); + } + return Future.value(true); + }); + } +} + class GroupEntity { final String name; final String id; @@ -134,16 +150,21 @@ class SubscribeItem { ///Podcast group, default Home. String group; + + ///sync to gpodder + bool syncWithGpodder; SubscribeItem(this.url, this.title, {this.subscribeState = SubscribeState.none, this.id = '', this.imgUrl = '', - this.group = ''}); + this.group = '', + this.syncWithGpodder = true}); } class GroupList extends ChangeNotifier { /// List of all gourps. final List _groups = []; + List get groups => _groups; final DBHelper _dbHelper = DBHelper(); @@ -155,6 +176,7 @@ class GroupList extends ChangeNotifier { /// Default false, true during loading groups from storage. bool _isLoading = false; + bool get isLoading => _isLoading; /// Svae ordered gourps info before saved. @@ -177,8 +199,9 @@ class GroupList extends ChangeNotifier { /// Add subsribe item SubscribeItem _subscribeItem; - setSubscribeItem(SubscribeItem item) async { + setSubscribeItem(SubscribeItem item, {bool syncGpodder = true}) async { _subscribeItem = item; + if (syncGpodder) _syncAdd(item.url); await _start(); } @@ -187,6 +210,13 @@ class GroupList extends ChangeNotifier { notifyListeners(); } + Future _syncAdd(String rssUrl) async { + final check = await _checkGpodderLoggedin(); + if (check) { + await _addStorage.addList([rssUrl]); + } + } + Future _start() async { if (_created == false) { await _createIsolate(); @@ -197,7 +227,8 @@ class GroupList extends ChangeNotifier { _subscribeItem.url, _subscribeItem.title, _subscribeItem.imgUrl, - _subscribeItem.group + _subscribeItem.group, + _subscribeItem.syncWithGpodder ]); } } @@ -217,7 +248,8 @@ class GroupList extends ChangeNotifier { _subscribeItem.url, _subscribeItem.title, _subscribeItem.imgUrl, - _subscribeItem.group + _subscribeItem.group, + _subscribeItem.syncWithGpodder ]); } else if (message is List) { _setCurrentSubscribeItem(SubscribeItem( @@ -238,6 +270,65 @@ class GroupList extends ChangeNotifier { }); } + ///Set gpodder sync + final _loginInfp = KeyValueStorage(gpodderApiKey); + final _addStorage = KeyValueStorage(gpodderAddKey); + final _removeStorage = KeyValueStorage(gpodderRemoveKey); + final _remoteAddStorage = KeyValueStorage(gpodderRemoteAddKey); + final _remoteRemoveStorage = KeyValueStorage(gpodderRemoteRemoveKey); + + Future _checkGpodderLoggedin() async { + final loginInfo = await _loginInfp.getStringList(); + return loginInfo.isNotEmpty; + } + + Future gpodderSyncNow() async { + final addList = await _remoteAddStorage.getStringList(); + final removeList = await _remoteRemoveStorage.getStringList(); + + if (removeList.isNotEmpty) { + for (var rssLink in removeList) { + final exist = await _dbHelper.checkPodcast(rssLink); + if (exist != '') { + await _unsubscribe(exist); + } + } + await _remoteAddStorage.clearList(); + } + if (addList.isNotEmpty) { + for (var rssLink in addList) { + final exist = await _dbHelper.checkPodcast(rssLink); + if (exist == '') { + var item = SubscribeItem(rssLink, rssLink, group: 'Home'); + _subscribeItem = item; + await _start(); + + await Future.delayed(Duration(milliseconds: 200)); + } + } + await _remoteRemoveStorage.clearList(); + } + } + + void setWorkManager(int hour) { + Workmanager.initialize( + callbackDispatcher, + isInDebugMode: false, + ); + Workmanager.registerPeriodicTask("2", "gpodder_sync", + frequency: Duration(hours: hour), + initialDelay: Duration(seconds: 10), + constraints: Constraints( + networkType: NetworkType.connected, + )); + developer.log('work manager init done + (gpodder sync)'); + } + + Future cancelWork() async { + await Workmanager.cancelByUniqueName('2'); + developer.log('work job cancelled'); + } + void addToOrderChanged(PodcastGroup group) { _orderChanged.add(group); notifyListeners(); @@ -261,6 +352,7 @@ class GroupList extends ChangeNotifier { @override void addListener(VoidCallback listener) { loadGroups().then((value) => super.addListener(listener)); + gpodderSyncNow(); } @override @@ -414,7 +506,14 @@ class GroupList extends ChangeNotifier { } /// Unsubscribe podcast - Future removePodcast(String id) async { + Future _syncRemove(String rssUrl) async { + final check = await _checkGpodderLoggedin(); + if (check) { + await _removeStorage.addList([rssUrl]); + } + } + + Future _unsubscribe(String id) async { _isLoading = true; notifyListeners(); for (var group in _groups) { @@ -429,7 +528,15 @@ class GroupList extends ChangeNotifier { notifyListeners(); } - saveOrder(PodcastGroup group) async { + Future removePodcast( + PodcastLocal podcast, + ) async { + _syncRemove(podcast.rssUrl); + final id = podcast.id; + await _unsubscribe(id); + } + + Future saveOrder(PodcastGroup group) async { group.podcastList = group.orderedPodcasts.map((e) => e.id).toList(); await _saveGroup(); await group.getPodcasts(); @@ -563,6 +670,13 @@ Future subIsolateEntryPoint(SendPort sendPort) async { } await dbHelper.savePodcastRss(p, uuid); + // if (item.syncWithGpodder) { + // final gpodder = Gpodder(); + // await gpodder.updateChange({ + // 'add': [item.url] + // }); + // } + sendPort.send([item.title, item.url, 3, uuid]); await Future.delayed(Duration(seconds: 2)); @@ -600,9 +714,9 @@ Future subIsolateEntryPoint(SendPort sendPort) async { } subReceivePort.distinct().listen((message) { - if (message is List) { + if (message is List) { items.add(SubscribeItem(message[0], message[1], - imgUrl: message[2], group: message[3])); + imgUrl: message[2], group: message[3], syncWithGpodder: message[4])); if (!_running) { _subscribe(items.first); _running = true; diff --git a/lib/state/search_state.dart b/lib/state/search_state.dart index 0246fb1..18425ae 100644 --- a/lib/state/search_state.dart +++ b/lib/state/search_state.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../type/searchpodcast.dart'; +import '../type/search_api/searchpodcast.dart'; class SearchState extends ChangeNotifier { final List _subscribedList = []; diff --git a/lib/state/setting_state.dart b/lib/state/setting_state.dart index d40171c..c68a066 100644 --- a/lib/state/setting_state.dart +++ b/lib/state/setting_state.dart @@ -161,7 +161,7 @@ class SettingState extends ChangeNotifier { } Future cancelWork() async { - await Workmanager.cancelAll(); + await Workmanager.cancelByUniqueName('1'); developer.log('work job cancelled'); } diff --git a/lib/type/search_api/itunes_podcast.dart b/lib/type/search_api/itunes_podcast.dart new file mode 100644 index 0000000..a1d2178 --- /dev/null +++ b/lib/type/search_api/itunes_podcast.dart @@ -0,0 +1,67 @@ +import 'package:intl/intl.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import 'searchpodcast.dart'; + +part 'itunes_podcast.g.dart'; + +@JsonSerializable() +class ItunesSearchResult

{ + @_ConvertP() + final List

results; + final int resultCount; + ItunesSearchResult({this.resultCount, this.results}); + + factory ItunesSearchResult.fromJson(Map json) => + _$ItunesSearchResultFromJson

(json); + Map toJson() => _$ItunesSearchResultToJson(this); +} + +class _ConvertP

implements JsonConverter { + const _ConvertP(); + @override + P fromJson(Object json) { + return ItunesPodcast.fromJson(json) as P; + } + + @override + Object toJson(P object) { + return object; + } +} + +@JsonSerializable() +class ItunesPodcast { + final String artistName; + final String collectionName; + final String feedUrl; + final String artworkUrl600; + final String releaseDate; + final int collectionId; + + ItunesPodcast( + {this.artistName, + this.collectionName, + this.feedUrl, + this.artworkUrl600, + this.releaseDate, + this.collectionId}); + + factory ItunesPodcast.fromJson(Map json) => + _$ItunesPodcastFromJson(json); + Map toJson() => _$ItunesPodcastToJson(this); + + int get latestPubDate => DateFormat('YYYY-MM-DDTHH:MM:SSZ', 'en_US') + .parse(releaseDate) + .millisecondsSinceEpoch; + OnlinePodcast get toOnlinePodcast => OnlinePodcast( + earliestPubDate: 0, + title: collectionName, + count: 0, + description: '', + image: artworkUrl600, + latestPubDate: latestPubDate, + rss: feedUrl, + publisher: artistName, + id: collectionId.toString()); +} diff --git a/lib/type/search_api/itunes_podcast.g.dart b/lib/type/search_api/itunes_podcast.g.dart new file mode 100644 index 0000000..58e0e0a --- /dev/null +++ b/lib/type/search_api/itunes_podcast.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'itunes_podcast.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ItunesSearchResult

_$ItunesSearchResultFromJson

( + Map json) { + return ItunesSearchResult

( + resultCount: json['resultCount'] as int, + results: (json['results'] as List)?.map(_ConvertP

().fromJson)?.toList(), + ); +} + +Map _$ItunesSearchResultToJson

( + ItunesSearchResult

instance) => + { + 'results': instance.results?.map(_ConvertP

().toJson)?.toList(), + 'resultCount': instance.resultCount, + }; + +ItunesPodcast _$ItunesPodcastFromJson(Map json) { + return ItunesPodcast( + artistName: json['artistName'] as String, + collectionName: json['collectionName'] as String, + feedUrl: json['feedUrl'] as String, + artworkUrl600: json['artworkUrl600'] as String, + releaseDate: json['releaseDate'] as String, + ); +} + +Map _$ItunesPodcastToJson(ItunesPodcast instance) => + { + 'artistName': instance.artistName, + 'collectionName': instance.collectionName, + 'feedUrl': instance.feedUrl, + 'artworkUrl600': instance.artworkUrl600, + 'releaseDate': instance.releaseDate, + }; diff --git a/lib/type/search_genre.dart b/lib/type/search_api/search_genre.dart similarity index 100% rename from lib/type/search_genre.dart rename to lib/type/search_api/search_genre.dart diff --git a/lib/type/search_top_podcast.dart b/lib/type/search_api/search_top_podcast.dart similarity index 100% rename from lib/type/search_top_podcast.dart rename to lib/type/search_api/search_top_podcast.dart diff --git a/lib/type/search_top_podcast.g.dart b/lib/type/search_api/search_top_podcast.g.dart similarity index 100% rename from lib/type/search_top_podcast.g.dart rename to lib/type/search_api/search_top_podcast.g.dart diff --git a/lib/type/searchepisodes.dart b/lib/type/search_api/searchepisodes.dart similarity index 100% rename from lib/type/searchepisodes.dart rename to lib/type/search_api/searchepisodes.dart diff --git a/lib/type/searchepisodes.g.dart b/lib/type/search_api/searchepisodes.g.dart similarity index 100% rename from lib/type/searchepisodes.g.dart rename to lib/type/search_api/searchepisodes.g.dart diff --git a/lib/type/searchpodcast.dart b/lib/type/search_api/searchpodcast.dart similarity index 100% rename from lib/type/searchpodcast.dart rename to lib/type/search_api/searchpodcast.dart diff --git a/lib/type/searchpodcast.g.dart b/lib/type/search_api/searchpodcast.g.dart similarity index 100% rename from lib/type/searchpodcast.g.dart rename to lib/type/search_api/searchpodcast.g.dart diff --git a/lib/util/custom_widget.dart b/lib/util/custom_widget.dart index 434eb87..fb90b1e 100644 --- a/lib/util/custom_widget.dart +++ b/lib/util/custom_widget.dart @@ -924,9 +924,10 @@ class IconPainter extends StatelessWidget { /// A dot just a dot. class DotIndicator extends StatelessWidget { - DotIndicator({this.radius = 8, Key key}) + DotIndicator({this.radius = 8, this.color, Key key}) : assert(radius > 0), super(key: key); + final Color color; final double radius; @override @@ -934,8 +935,8 @@ class DotIndicator extends StatelessWidget { return Container( width: radius, height: radius, - decoration: - BoxDecoration(shape: BoxShape.circle, color: context.accentColor)); + decoration: BoxDecoration( + shape: BoxShape.circle, color: color ?? context.accentColor)); } } diff --git a/lib/util/extension_helper.dart b/lib/util/extension_helper.dart index 1cb407b..9b44553 100644 --- a/lib/util/extension_helper.dart +++ b/lib/util/extension_helper.dart @@ -29,6 +29,8 @@ extension IntExtension on int { var date = DateTime.fromMillisecondsSinceEpoch(this, isUtc: true); var difference = DateTime.now().toUtc().difference(date); if (difference.inMinutes < 30) { + return s.minsAgo(difference.inMinutes); + } else if (difference.inMinutes < 60) { return s.hoursAgo(0); } else if (difference.inHours < 24) { return s.hoursAgo(difference.inHours); diff --git a/lib/util/general_dialog.dart b/lib/util/general_dialog.dart index 920dc06..64a97da 100644 --- a/lib/util/general_dialog.dart +++ b/lib/util/general_dialog.dart @@ -33,6 +33,7 @@ Future generalDialog(BuildContext context, Future generalSheet(BuildContext context, {Widget child, String title}) async => await showModalBottomSheet( + useRootNavigator: true, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -41,21 +42,21 @@ Future generalSheet(BuildContext context, {Widget child, String title}) async => elevation: 2, context: context, builder: (context) { + final statusHeight = MediaQuery.of(context).padding.top; return SafeArea( - child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: + BoxConstraints(maxHeight: context.height - statusHeight - 80), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: EdgeInsets.only(top: 10.0, bottom: 2.0), - child: Container( - height: 4, - width: 25, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(2.0), - color: context.primaryColorDark), - ), + Container( + height: 4, + width: 25, + margin: EdgeInsets.only(top: 10.0, bottom: 2.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2.0), + color: context.primaryColorDark), ), Padding( padding: EdgeInsets.only( @@ -69,7 +70,7 @@ Future generalSheet(BuildContext context, {Widget child, String title}) async => ), ), Divider(height: 1), - child, + Flexible(child: SingleChildScrollView(child: child)), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index a9a5d11..37fc948 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,16 +13,19 @@ dependencies: sdk: flutter auto_animated: ^2.1.0 audio_session: ^0.0.7 - cached_network_image: ^2.3.1 + cached_network_image: ^2.3.2+1 color_thief_flutter: ^1.0.2 + cookie_jar: ^1.0.0 cupertino_icons: ^1.0.0 connectivity: ^0.4.9 + device_info: ^0.4.2+7 dio: ^3.0.10 + dio_cookie_manager: ^1.0.0 extended_nested_scroll_view: ^1.0.1 effective_dart: ^1.2.4 - equatable: ^1.2.3 + equatable: ^1.2.5 feature_discovery: ^0.10.0 - file_picker: ^1.12.0 + file_picker: ^2.0.0 flutter_html: ^0.11.1 flutter_downloader: ^1.5.0 fluttertoast: ^4.0.0 @@ -37,15 +40,15 @@ dependencies: intl: ^0.16.1 json_serializable: ^3.4.1 json_annotation: ^3.0.1 - path_provider: ^1.6.14 + path_provider: ^1.6.16 permission_handler: ^5.0.1 provider: ^4.3.2 rxdart: ^0.24.1 sqflite: ^1.3.1 shared_preferences: ^0.5.10 tuple: ^1.0.3 - url_launcher: ^5.5.3 - uuid: ^2.2.0 + url_launcher: ^5.6.0 + uuid: ^2.2.2 xml: ^4.2.0 workmanager: ^0.2.3 wc_flutter_share: ^0.2.2