Sync with gpodder.net.

This commit is contained in:
stonegate 2020-09-23 22:19:07 +08:00
parent 064d63350d
commit 719d9c8cc0
40 changed files with 2651 additions and 310 deletions

BIN
assets/gpodder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,365 @@
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
// This is a library that provides messages for a pt locale. All the
// messages from the main program should be duplicated here with the same
// function name.
// Ignore issues from commonly used lints in this file.
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
// ignore_for_file:unused_import, file_names
import 'package:intl/intl.dart';
import 'package:intl/message_lookup_by_library.dart';
final messages = new MessageLookup();
typedef String MessageIfAbsent(String messageStr, List<dynamic> 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: '${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: '${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: '${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: '${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(_) => <String, Function> {
"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
};
}

View File

@ -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(_) => <String, Function> {
@ -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
};
}

View File

@ -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<PopupMenu> {
final s = context.s;
var file = File(path);
try {
Map<String, List<OmplOutline>> data = PodcastsBackup.parseOMPL(file);
final opml = file.readAsStringSync();
Map<String, List<OmplOutline>> 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<PopupMenu> {
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');

View File

@ -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<DiscoveryPage> {
));
Future<List<OnlinePodcast>> _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<DiscoveryPage> {
if (snapshot.hasData && snapshot.data.isNotEmpty) {
final history = snapshot.data;
return SizedBox(
height: 50,
child: Wrap(
direction: Axis.horizontal,
children: history
.map<Widget>((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<DiscoveryPage> {
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<List<OnlinePodcast>> _getTopPodcasts({Genre genre, int page}) async {
final searchEngine = SearchEngine();
final searchEngine = ListenNotesSearch();
var searchResult = await searchEngine.fetchBestPodcast(
genre: genre.id,
page: page,

View File

@ -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<SearchList> {
Future<List<OnlinePodcast>> _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<SearchResultDetail>
Future<List<OnlineEpisode>> _getEpisodes(
{String id, int nextEpisodeDate}) async {
var searchEngine = SearchEngine();
var searchEngine = ListenNotesSearch();
var searchResult = await searchEngine.fetchEpisode(
id: id, nextEpisodeDate: nextEpisodeDate);
_nextEpisdoeDate = searchResult.nextEpisodeDate;

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

683
lib/l10n/intl_pt.arb Normal file
View File

@ -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": {}
}
}
}

View File

@ -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": {

View File

@ -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<void> addList(List<String> addList) async {
final list = await getStringList();
await saveStringList(list..addAll(addList));
}
Future<void> clearList() async {
await saveStringList([]);
}
}

View File

@ -242,7 +242,7 @@ class DBHelper {
return ['', ''];
}
Future delPodcastLocal(String id) async {
Future<void> delPodcastLocal(String id) async {
var dbClient = await database;
await dbClient.rawDelete('DELETE FROM PodcastLocal WHERE id =?', [id]);
List<Map> 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<void> saveHistory(PlayHistory history) async {

View File

@ -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(

View File

@ -390,7 +390,7 @@ class _PodcastSettingState extends State<PodcastSetting> {
FlatButton(
splashColor: Colors.red.withAlpha(70),
onPressed: () async {
await groupList.removePodcast(widget.podcastLocal.id);
await groupList.removePodcast(widget.podcastLocal);
Navigator.of(context).pop();
},
child:

View File

@ -59,7 +59,7 @@ class _AboutPodcastState extends State<AboutPodcast> {
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,

View File

@ -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<void> _initDio() async {
final dir = await getApplicationDocumentsDirectory();
var cookieJar = PersistCookieJar(dir: "${dir.path}/.cookies/");
_dio.interceptors.add(CookieManager(cookieJar));
}
Future<int> 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: <String, String>{'authorization': basicAuth}));
status = response.statusCode;
} catch (e) {
developer.log(e.toString(), name: 'gpoderr login error');
return 0;
}
return status;
}
Future<int> 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<void> _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<int> 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<int> 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<String> 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<int> 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<int> 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<String>();
final removeList = changes['remove'].cast<String>();
print(removeList);
await _storage.saveStringList([username, deviceId, timeStamp.toString()]);
await _remoteAddStorage.addList(addList);
await _remoteRemoveStorage.addList(removeList);
}
return status;
}
Future<int> 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;
}
}

View File

@ -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<PodcastGroup> 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 = <String, List<OmplOutline>>{};
var opml = file.readAsStringSync();
// var opml = file.readAsStringSync();
var content = xml.XmlDocument.parse(opml);
var title =
content.findAllElements('head').first.findElements('title').first.text;

View File

@ -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<SearchPodcast<dynamic>> searchPodcasts(
{String searchText, int nextOffset}) async {
@ -51,3 +51,16 @@ class SearchEngine {
return searchResult;
}
}
class ItunesSearch {
Future<SearchPodcast<dynamic>> 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;
}
}

View File

@ -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<DataBackup> {
final _gpodder = Gpodder();
var _syncing = false;
Future<File> _exportOmpl(BuildContext context) async {
var groups = context.read<GroupList>().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<DataBackup> {
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<void> _importSetting(String path, BuildContext context) async {
final s = context.s;
var settings = context.read<SettingState>();
var file = File(path);
@ -89,23 +98,83 @@ class _DataBackupState extends State<DataBackup> {
}
}
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<void> _logout() async {
await _gpodder.logout();
final subscribeWorker = context.read<GroupList>();
subscribeWorker.cancelWork();
Fluttertoast.showToast(
msg: 'Logout successfully',
gravity: ToastGravity.BOTTOM,
);
if (mounted) setState(() {});
}
Future<List<String>> _getLoginInfo() async {
final storage = KeyValueStorage(gpodderApiKey);
return await storage.getStringList();
}
Future<void> _syncNow() async {
setState(() {
_syncing = true;
});
final gpodder = Gpodder();
final status = await gpodder.getChanges();
if (status == 200) {
final groupList = context.read<GroupList>();
await gpodder.updateChange();
await groupList.gpodderSyncNow();
}
if (mounted) {
setState(() {
_syncing = false;
});
}
}
Future<Tuple2<int, int>> _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<DataBackup> {
Padding(
padding: EdgeInsets.all(10.0),
),
FutureBuilder<List<String>>(
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<List<String>>(
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<Tuple2<int, int>>(
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<DataBackup> {
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<DataBackup> {
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<FormFieldState<String>> _passwordFieldKey =
GlobalKey<FormFieldState<String>>();
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _gpodder = Gpodder();
Future<void> _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<void> _getSubscriptions(Gpodder gpodder) async {
var subscribeWorker = context.read<GroupList>();
var rssExp = RegExp(r'^(https?):\/\/(.*)');
final opml = await gpodder.getAllPodcast();
if (opml != '') {
Map<String, List<OmplOutline>> 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<Color>(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<SystemUiOverlayStyle>(
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<String> onSaved;
final FormFieldValidator<String> validator;
final ValueChanged<String> onFieldSubmitted;
@override
_PasswordFieldState createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> {
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',
),
),
),
);
}
}

View File

@ -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<PlayedHistory>
Theme.of(context).accentColorBrightness,
),
child: Scaffold(
backgroundColor: Theme.of(context).primaryColor,
backgroundColor: context.primaryColor,
body: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxScrolled) {

View File

@ -62,6 +62,7 @@ class _SettingsState extends State<Settings> {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
@ -71,9 +72,7 @@ class _SettingsState extends State<Settings> {
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(

View File

@ -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: <Widget>[
Selector<SettingState, Tuple2<bool, int>>(
selector: (_, settings) =>
Tuple2(settings.autoUpdate, settings.updateInterval),
builder: (_, data, __) => Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <Widget>[
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: <int>[1, 2, 4, 8, 24, 48]
.map<DropdownMenuItem<int>>((e) {
return DropdownMenuItem<int>(
value: e, child: Text(s.hoursCount(e)));
}).toList()),
),
Divider(height: 1),
],
),
],
child: Selector<SettingState, Tuple2<bool, int>>(
selector: (_, settings) =>
Tuple2(settings.autoUpdate, settings.updateInterval),
builder: (_, data, __) => Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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: <int>[1, 2, 4, 8, 24, 48]
.map<DropdownMenuItem<int>>((e) {
return DropdownMenuItem<int>(
value: e, child: Text(s.hoursCount(e)));
}).toList()),
),
Divider(height: 1),
],
),
),
),
),

View File

@ -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<SettingState, int>(
selector: (_, setting) => setting.showNotesFontIndex,

View File

@ -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<PodcastGroup> _groups = [];
List<PodcastGroup> 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<void> _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<bool> _checkGpodderLoggedin() async {
final loginInfo = await _loginInfp.getStringList();
return loginInfo.isNotEmpty;
}
Future<void> 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<void> removePodcast(String id) async {
Future<void> _syncRemove(String rssUrl) async {
final check = await _checkGpodderLoggedin();
if (check) {
await _removeStorage.addList([rssUrl]);
}
}
Future<void> _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<void> removePodcast(
PodcastLocal podcast,
) async {
_syncRemove(podcast.rssUrl);
final id = podcast.id;
await _unsubscribe(id);
}
Future<void> saveOrder(PodcastGroup group) async {
group.podcastList = group.orderedPodcasts.map((e) => e.id).toList();
await _saveGroup();
await group.getPodcasts();
@ -563,6 +670,13 @@ Future<void> 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<void> subIsolateEntryPoint(SendPort sendPort) async {
}
subReceivePort.distinct().listen((message) {
if (message is List<String>) {
if (message is List<dynamic>) {
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;

View File

@ -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<OnlinePodcast> _subscribedList = [];

View File

@ -161,7 +161,7 @@ class SettingState extends ChangeNotifier {
}
Future cancelWork() async {
await Workmanager.cancelAll();
await Workmanager.cancelByUniqueName('1');
developer.log('work job cancelled');
}

View File

@ -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<P> {
@_ConvertP()
final List<P> results;
final int resultCount;
ItunesSearchResult({this.resultCount, this.results});
factory ItunesSearchResult.fromJson(Map<String, dynamic> json) =>
_$ItunesSearchResultFromJson<P>(json);
Map<String, dynamic> toJson() => _$ItunesSearchResultToJson(this);
}
class _ConvertP<P> implements JsonConverter<P, Object> {
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<String, dynamic> json) =>
_$ItunesPodcastFromJson(json);
Map<String, dynamic> 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());
}

View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'itunes_podcast.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ItunesSearchResult<P> _$ItunesSearchResultFromJson<P>(
Map<String, dynamic> json) {
return ItunesSearchResult<P>(
resultCount: json['resultCount'] as int,
results: (json['results'] as List)?.map(_ConvertP<P>().fromJson)?.toList(),
);
}
Map<String, dynamic> _$ItunesSearchResultToJson<P>(
ItunesSearchResult<P> instance) =>
<String, dynamic>{
'results': instance.results?.map(_ConvertP<P>().toJson)?.toList(),
'resultCount': instance.resultCount,
};
ItunesPodcast _$ItunesPodcastFromJson(Map<String, dynamic> 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<String, dynamic> _$ItunesPodcastToJson(ItunesPodcast instance) =>
<String, dynamic>{
'artistName': instance.artistName,
'collectionName': instance.collectionName,
'feedUrl': instance.feedUrl,
'artworkUrl600': instance.artworkUrl600,
'releaseDate': instance.releaseDate,
};

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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)),
],
),
),

View File

@ -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