From dd2be50d17e54cd8a50164e3a2e51055372d42ea Mon Sep 17 00:00:00 2001 From: Joseph Alves Date: Wed, 20 May 2026 11:44:22 -0300 Subject: [PATCH 1/9] Add pt-BR --- composeApp/src/androidMain/res/xml/locale_config.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/src/androidMain/res/xml/locale_config.xml b/composeApp/src/androidMain/res/xml/locale_config.xml index 6bb55c30b..9fe7cd591 100644 --- a/composeApp/src/androidMain/res/xml/locale_config.xml +++ b/composeApp/src/androidMain/res/xml/locale_config.xml @@ -8,6 +8,7 @@ + From 83e40b2782b83d701a7771ddc633770a05f01855 Mon Sep 17 00:00:00 2001 From: Joseph Alves Date: Wed, 20 May 2026 11:46:03 -0300 Subject: [PATCH 2/9] Add pt-BR --- .../kotlin/com/nuvio/app/features/settings/AppLanguage.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt index 8e989c5e5..05abde857 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt @@ -9,6 +9,7 @@ import nuvio.composeapp.generated.resources.lang_greek import nuvio.composeapp.generated.resources.lang_indonesian import nuvio.composeapp.generated.resources.lang_italian import nuvio.composeapp.generated.resources.lang_polish +import nuvio.composeapp.generated.resources.lang_portuguese_brazil import nuvio.composeapp.generated.resources.lang_portuguese_portugal import nuvio.composeapp.generated.resources.lang_spanish import nuvio.composeapp.generated.resources.lang_turkish @@ -27,6 +28,7 @@ enum class AppLanguage( INDONESIAN("id", Res.string.lang_indonesian), ITALIAN("it", Res.string.lang_italian), POLISH("pl", Res.string.lang_polish), + PORTUGUESE BRAZIL("pt-BR", Res.string.lang_portuguese_brazil), PORTUGUESE("pt", Res.string.lang_portuguese_portugal), SPANISH("es", Res.string.lang_spanish), TURKISH("tr", Res.string.lang_turkish), From 695da820e9fffba8a04042d3f8f1e9d44ef018a2 Mon Sep 17 00:00:00 2001 From: Joseph Alves Date: Wed, 20 May 2026 15:14:00 -0300 Subject: [PATCH 3/9] Add pt-BR --- .../composeResources/values-pt-BR/strings.xml‎ | 1344 +++++++++++++++++ 1 file changed, 1344 insertions(+) create mode 100644 composeApp/src/commonMain/composeResources/values-pt-BR/strings.xml‎ diff --git a/composeApp/src/commonMain/composeResources/values-pt-BR/strings.xml‎ b/composeApp/src/commonMain/composeResources/values-pt-BR/strings.xml‎ new file mode 100644 index 000000000..49fb2960d --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-pt-BR/strings.xml‎ @@ -0,0 +1,1344 @@ + + Fontes de dados, agradecimentos e licenças da plataforma + Reconhecimento aberto e créditos do projeto + Voltar + Cancelar + Fechar + Excluir + Concluído + Editar + Importar + Próximo + OK + Reproduzir + Anterior + Remover + Reordenar + Redefinir para Padrão + Retomar + Tentar novamente + Salvar + Salvando… + Validar + Instalando + Addons + Ativo + %1$d catálogos + Configurável + Atualizando + %1$d recursos + Indisponível + Configurar addon + Excluir addon + Adicione uma URL de manifesto para começar a carregar catálogos, metadados, streams ou legendas no Nuvio. + Nenhum addon instalado ainda. + Insira uma URL de addon. + URL do Addon + Instalar Addon + Carregando detalhes do manifesto... + Validando a URL do manifesto e carregando detalhes do addon antes da instalação. + Verificando Addon + Falha na Instalação + %1$s foi validado e adicionado com sucesso. + Addon Instalado + Mover addon para baixo + Mover addon para cima + Ativo + Addons + Catálogos + Atualizar addon + Adicionar Addon + Addons Instalados + Visão Geral + %1$d regras de ID + Versão %1$s + Selecionado + Copiar JSON + %1$d coleção(ões), %2$d pasta(s) + Excluir "%1$s"? Isso não pode ser desfeito. + Excluir Coleção + Adicionar Catálogo + Adicionar Pasta + Todos os gêneros + Adicione catálogos dos seus addons instalados para definir o que esta pasta exibe. + Nenhuma fonte de catálogo ainda + Escolher + Emoji + URL da Imagem + Nenhuma + Capa + Criar Coleção + Concluído + Editar Coleção + Editar Pasta + Defina a identidade da pasta, apresentação e fontes de catálogo com a mesma estrutura do editor principal de coleções. + Adicione uma para começar. + Nenhuma pasta ainda + Pastas + Filtro de Gênero + Mostrar apenas a imagem de capa + Ocultar Título + Nova Pasta + Mostrar esta coleção acima de todos os catálogos regulares da página inicial. Múltiplas coleções fixadas seguem a ordem de criação da coleção. + Fixar Acima dos Catálogos + URL da imagem de backdrop (opcional) + Nome da pasta + URL do GIF animado (reproduz apenas ao focar) + Nome da coleção + Salvar Alterações + Salvar + Aparência + Básico + Fontes de Catálogo + Escolha os catálogos de addon que esta pasta deve agregar. + Selecionar Catálogos + Selecionar gênero + %1$d selecionado(s) + %1$d catálogos + %1$d selecionado(s) + Pôster + Quadrado + Largo + Combinar todos os catálogos em uma única aba + Mostrar Aba \"Todos\" + Reproduzir o GIF configurado em vez da capa estática quando disponível. + Mostrar GIF Quando Configurado + %1$d fonte(s) · %2$s + Formato do Tile + Linhas + Abas + Modo de Visualização + Fontes TMDB + Lista Pública + Produção + Emissora + Coleção + Pessoa + Diretor + Personalizado + Escolha uma fonte pronta. Você pode editar ou removê-la após adicionar. + Cole a URL de uma lista pública do TMDB ou apenas o número da URL. + Busque pelo nome do estúdio, ou cole um ID/URL de empresa do TMDB e adicione diretamente. + Insira um ID de emissora. Emissoras comuns estão disponíveis em Predefinições e filtros rápidos. + Busque o nome de uma coleção de filmes ou cole o ID da coleção do TMDB. + Insira um ID ou URL de pessoa do TMDB para construir uma linha a partir dos créditos de elenco. + Insira um ID ou URL de pessoa do TMDB para construir uma linha a partir dos créditos de diretor. + Construa uma linha dinâmica do TMDB usando filtros opcionais. Deixe campos vazios quando não precisar desse filtro. + Lista pública do TMDB + ID da Emissora + ID da Coleção + ID da Pessoa + Nome da empresa de produção, ID ou URL + ID ou URL do TMDB + https://www.themoviedb.org/list/8504994 ou 8504994 + 213 para Netflix, 49 para HBO, 2739 para Disney+ + 10 para Coleção Star Wars + Marvel Studios, 420, ou URL da empresa + 31 para Tom Hanks, ou URL da pessoa + Exemplos: Marvel Studios, 420, ou https://www.themoviedb.org/company/420. + Exemplo: Coleção Star Wars, Coleção Harry Potter, ou uma URL de coleção. + Exemplos de IDs: Netflix 213, HBO 49, Disney+ 2739. + Exemplo: https://www.themoviedb.org/list/8504994 ou 8504994. + Exemplo: https://www.themoviedb.org/person/31-tom-hanks ou 31. + Título de exibição + Exibido como nome da linha/aba. Se em branco, o Nuvio cria um a partir da fonte. + Filmes da Marvel, Originais Netflix, Pixar + Filmes do Tom Hanks, Atores Favoritos + Filmes do Christopher Nolan, Diretores Favoritos + Melhores Filmes de Ação, Dramas Coreanos, Animação 2024 + Resultados da Busca + Coleção do TMDB + Empresa do TMDB %1$d + Coleção do TMDB %1$d + Tipo + Filmes + Séries + Ambos + Ordenar + Filtros + Deixe campos vazios quando não precisar desse filtro. + Gêneros rápidos + Idiomas rápidos + Países rápidos + Palavras-chave rápidas + Estúdios rápidos + Emissoras rápidas + IDs de Gênero + Use números de gênero do TMDB. Separe múltiplos com vírgulas para AND, ou pipes para OR. + Data de lançamento ou exibição de + Data de lançamento ou exibição até + Use YYYY-MM-DD, por exemplo 2024-01-01. + Classificação mínima + Classificação máxima + Classificação do TMDB de 0 a 10. Exemplo: 7,0. + Votos mínimos + Use isso para evitar títulos obscuros com poucos votos. Exemplo: 100. + Idioma original + Use códigos de idioma de duas letras, por exemplo en, ko, ja, hi. + País de origem + Use códigos de país de duas letras, por exemplo US, KR, JP, IN. + IDs de Palavras-chave + Use números de palavra-chave do TMDB. Chips rápidos preenchem exemplos comuns. + 9715 para super-herói + IDs de Empresa + Use IDs de estúdio/empresa. Chips rápidos preenchem exemplos comuns. + 420 para Marvel Studios + IDs de Emissoras + Apenas para séries. Use IDs de emissoras como Netflix 213 ou HBO 49. + 213 para Netflix + Ano + Use um ano de quatro dígitos, por exemplo 2024. + Predefinições + Buscar + Adicionar Fonte + Adicionar Lista do Trakt + Editar Lista do Trakt + Listas do Trakt + Lista do Trakt + Buscar título, URL do Trakt, ou ID da lista + Use uma URL de lista pública do Trakt ou ID numérico da lista, ou busque por nome. + Fim de Semana, Vencedores de Prêmios + Resultados da Busca + Listas em Alta + Listas Populares + Direção + Ascendente + Descendente + Ordem da Lista + Adicionados Recentemente + Título + Lançados + Duração + Populares + Porcentagem + Votos + Ação + Aventura + Animação + Comédia + Terror + Ficção Científica + Drama + Crime + Reality + Inglês + Coreano + Japonês + Hindi + Espanhol + Estados Unidos + Coreia + Japão + Índia + Reino Unido + Super-herói + Baseado em Romance + Viagem no Tempo + Espaço + Marvel + Disney + Pixar + Lucasfilm + Warner Bros. + Netflix + HBO + Disney+ + Prime Video + Hulu + Original + Popular + Mais Bem Avaliados + Recentes + Mais Votados + Região de exibição + Código de país ISO 3166-1 onde o título está disponível. Exemplo: US, BR. + Regiões de exibição rápidas + IDs de provedores de exibição + Use IDs de provedores de exibição do TMDB. Separe múltiplos com vírgulas para E, ou pipes para OU. + 8|337|350 + Provedores de exibição rápidos + Netflix + Prime Video + Disney+ + Apple TV+ + Hulu + Lista do TMDB + Coleção de Filmes do TMDB + Produção + Emissora + Pessoa + Diretor + TMDB Discover + Crie uma para organizar seus catálogos. + Nenhuma coleção ainda + %1$d pasta(s) + Nenhum item encontrado + Pasta não encontrada + Coleções + Importar Coleções + JSON + Cole seu JSON de coleções abaixo. + Importar + Nova Coleção + Fixado + Todos + Suas Coleções + Feito com ❤️ por Tapframe e amigos + Versão %1$s (%2$s) + Desativado + Ativado + Pausar + Recarregar + Já tem uma conta? + Continuar sem conta + Criar Conta + Não tem uma conta? + E-mail + ou + Senha + Faça login para acessar sua biblioteca e progresso + Entrar + Cadastre-se para sincronizar seus dados entre dispositivos + Cadastrar + Seus dados serão armazenados apenas localmente + Assista tudo, em qualquer lugar + Bem-vindo de Volta + Biblioteca + Biblioteca do Trakt + Início + Biblioteca + Perfil + Busca + Faixas de Áudio + Áudio + Incorporado + Deslocamento Inferior + Fechar player + Cor + Reproduzindo agora + E%1$d + T%1$dE%2$d + T%1$dE%2$d • %3$s + Episódios + Tamanho da Fonte + %1$dsp + Bloquear controles do player + Nenhuma faixa de áudio disponível + Nenhum episódio disponível + Nenhuma stream encontrada + Nenhuma + Contorno + Episódios + Fontes + Streams + Erro de reprodução + Reproduzindo + Toque para buscar legendas + Voltar + Redefinir Padrões + Preencher + Ajustar + Zoom + Retroceder 10 segundos + -%1$ds + +%1$ds + -%1$ds + +%1$ds + Avançar 10 segundos + Fontes + Estilo + Legendas + Legendas + Brilho %1$s + Volume %1$s + Silenciado + Baixado + Exibe + A definir + Toque para desbloquear + Faixa %1$d + Desbloquear controles do player + Você está assistindo + Adicionar Perfil + Limpar busca + Descobrir + Addons instalados falharam ao retornar resultados de busca válidos. + Busca falhou + Instale e valide pelo menos um addon antes de buscar. + Nenhum addon ativo + Catálogos pesquisáveis instalados não retornaram correspondências para esta consulta. + Nenhum resultado encontrado + Seus addons instalados não expõem busca de catálogo. + Nenhum catálogo pesquisável + Buscar filmes, séries... + Buscas Recentes + Remover busca recente + Sobre + Geral + Conta + Addons + Layout + Conteúdo & Descoberta + Continuar Assistindo + Debrid + Layout da Página Inicial + Integrações + Licenças & Atribuições + Classificações do MDBList + Página de Detalhes + Notificações + Reprodução + Plugins + Estilo do Card de Pôster + Configurações + Apoiadores & Contribuidores + Enriquecimento do TMDB + Trakt + SOBRE + Conta e status de sincronização + CONTA + Estrutura da página inicial e estilos de pôster + Baixar a versão mais recente + Verificar atualizações + Gerenciar addons e fontes de descoberta. + Gerencie seus filmes e episódios baixados. + Downloads + GERAL + Gerenciar integrações disponíveis + Gerencie alertas de lançamento de episódios e envie uma notificação de teste. + Mude para um perfil diferente. + Alternar Perfil + Abrir tela de conexão do Trakt + Nenhuma configuração encontrada. + Buscar configurações... + RESULTADOS + LICENÇA DO APP + DADOS & SERVIÇOS + LICENÇA DE REPRODUÇÃO + Nuvio Mobile + Código-fonte e termos de licença estão disponíveis no repositório do projeto. + Licenciado sob a GNU General Public License v3.0. + The Movie Database (TMDB) + O Nuvio usa a API do TMDB para metadados de filmes e séries, artes, trailers, elenco, detalhes de produção, coleções e recomendações. Este produto usa a API do TMDB, mas não é endossado ou certificado pelo TMDB. + Conjuntos de Dados Não Comerciais do IMDb + O Nuvio usa os Conjuntos de Dados Não Comerciais do IMDb, incluindo title.ratings.tsv.gz, para classificações e contagem de votos do IMDb. Informações cortesia do IMDb (https://www.imdb.com). Usado com permissão. Dados do IMDb são para uso pessoal e não comercial sob os termos do IMDb. + Trakt + O Nuvio conecta-se ao Trakt para autenticação de conta, histórico de assistidos, sincronização de progresso, dados da biblioteca, classificações, listas e comentários. O Nuvio não é afiliado ou endossado pelo Trakt. + MDBList + O Nuvio usa o MDBList para classificações e dados de provedores externos de pontuação. O Nuvio não é afiliado ou endossado pelo MDBList. + IntroDB + O Nuvio usa a API do IntroDB para timestamps de intro, recapitulação, créditos e prévia fornecidos pela comunidade, usados pelos controles de pular. O Nuvio não é afiliado ou endossado pelo IntroDB. + MPVKit + Usado para reprodução em builds para iOS. + O código-fonte do MPVKit isolado é licenciado sob LGPL v3.0. Os bundles do MPVKit, incluindo bibliotecas libmpv e FFmpeg, também são licenciados sob LGPL v3.0. + AndroidX Media3 ExoPlayer 1.8.0 + Usado para reprodução em builds para Android. + Licenciado sob a Apache License, Version 2.0. + Carregando suas listas do Trakt… + Escolha onde salvar este título no Trakt + Doar + Ir para detalhes + Remover + Começar do início + Reproduzir + %1$d/10 + Avaliação + Spoiler + Nenhuma avaliação do Trakt disponível ainda. + %1$d curtidas + Este comentário contém spoilers. + Este comentário contém spoilers e foi ocultado. + Comentários + Trailer + %1$s (%2$d) + Trailers + Nenhum episódio concluído + Nenhum download ainda + %1$d episódio(s) baixado(s) + Ativo + Filmes + Séries + Mostrar Downloads + Concluído • %1$s + Baixando • %1$s + Falhou + Pausado • %1$s + Assistido + Temporada %1$d + Especiais + Continue de onde parou + Adicionar à biblioteca + Marcar como não assistido + Marcar como assistido + Remover da biblioteca + Ver Todos + Reproduzir manualmente + Logo de %1$s + Conta + Excluir Conta + Isso excluirá permanentemente sua conta e todos os dados associados. + Esta ação não pode ser desfeita. Todos os seus dados, perfis e histórico de sincronização serão removidos permanentemente. + Excluir Conta? + E-mail + Não conectado + Sair + Você será retornado à tela de login. + Sair? + Status + Anônimo + Conectado + Preto AMOLED + Usar fundos pretos puros para telas OLED. + Idioma do App + Escolher Idioma + Configurações para a seção Continuar Assistindo. + Liquid Glass + Usar a barra de abas nativa do iPhone no iOS 26 e posterior. A alternância instantânea de perfil pela barra de abas não está disponível enquanto isso estiver ativado. + Ajuste largura do card e raio dos cantos. + EXIBIÇÃO + PÁGINA INICIAL + TEMA + Coleção • %1$s + Nome de Exibição + Instale um addon com catálogos compatíveis com board para configurar as linhas da Página Inicial. + Nenhum catálogo da página inicial + Fonte do hero + Oculto + Manter Página Inicial focada + %1$s • Limite atingido (máx %2$d) + Nenhuma fonte de hero selecionada + Não no hero + Remova o fixar no topo da coleção para mover + Fixado + Fixado no topo + Reordenar + CATÁLOGOS + CATÁLOGOS & COLEÇÕES + COLEÇÕES + Layout da Página Inicial + Catálogos do Hero + %1$d de %2$d selecionado(s) + Mostrar Seção Hero + Exibir carrossel hero no topo da página inicial. + Ocultar Conteúdo Não Lançado + Ocultar filmes e séries que ainda não foram lançados. + Ocultar Linha Sublinhada do Catálogo + Remover a linha de destaque abaixo dos títulos de catálogo e coleção em todo o app. + %1$d de %2$d catálogos visíveis • %3$d fontes de hero selecionadas + Abra um catálogo apenas quando precisar renomeá-lo ou reordená-lo. + Visível + Ocultar valor + Player, legendas e reprodução automática + Raio dos Cantos + Estilo do Card de Pôster + Largura + Personalizado + Ajuste largura do card e raio dos cantos. + Ocultar rótulos + Pôsteres em Paisagem + Pré-visualização ao Vivo + %1$s (%2$s) + Raio dos cantos: %1$ddp + Altura: %1$ddp + Largura: %1$ddp + Clássico + Pílula + Arredondado + Nítido + Sutil + Equilibrado + Conforto + Compacto + Denso + Grande + Padrão + Mostrar valor + Mostrar um popup para continuar de onde parou ao abrir o app após sair do player. + Prompt de retomada ao iniciar + Desfocar thumbnails do próximo episódio em Continuar Assistindo para evitar spoilers. + Desfocar Não Assistidos em Continuar Assistindo + Incluir episódios futuros em Continuar Assistindo antes de serem exibidos. + Mostrar Episódios Próximos Não Exibidos + ORDEM DE CLASSIFICAÇÃO + Ordem de Classificação + Padrão + Classificar todos os itens por recenticidade + Estilo Streaming + Itens lançados primeiro, futuros no final + Estilo do Card de Pôster + AO INICIAR + COMPORTAMENTO DO PRÓXIMO + VISIBILIDADE + Exibir a prateleira Continuar Assistindo na tela inicial. + Mostrar Continuar Assistindo + Pôster + Card de pôster com foco na arte + Largo + Card horizontal com informações densas + Mostrar próximo episódio com base no episódio assistido mais avançado. Desative para reassistir e usar o episódio assistido mais recentemente. + Próximo a Partir do Episódio Mais Avançado + Preferir thumbnails de episódios quando disponíveis. + Preferir Thumbnails de Episódios em Continuar Assistindo + PÁGINA INICIAL + FONTES + Instale, remova, atualize e ordene suas fontes de conteúdo. + Instale repositórios de scraper JavaScript e teste provedores internamente. + Ajuste layout da página inicial, visibilidade de conteúdo e comportamento de pôsteres + Configurações para as telas de detalhes e episódios. + Crie agrupamentos personalizados de catálogos com pastas exibidas na Página Inicial. + Integrações + Controles de enriquecimento de metadados + Provedores externos de classificações + Fontes de contas em nuvem experimentais + Debrid + O suporte a Debrid é experimental e pode ser mantido, alterado ou removido posteriormente. + Ativar fontes + Exibir resultados reproduzíveis de contas conectadas. + Adicione uma chave de API primeiro. + Conta + Conecte sua conta do Torbox. + Chave de API do Torbox + Insira sua chave de API do Torbox. + Insira a chave de API do Torbox + Não definido + Reprodução Instantânea + Preparar links + Resolver as primeiras fontes antes do início da reprodução. + Fontes a preparar + Use uma contagem menor quando possível. Os serviços de Debrid podem limitar a taxa de quantos links podem ser resolvidos em um período de tempo. Abrir um filme ou episódio pode contar para esses limites mesmo se você não pressionar Assistir, porque os links são preparados antecipadamente. + 1 fonte + %1$d fontes + Formatação + Modelo de nome + Controla como os nomes das fontes aparecem. + Modelo de descrição + Controla os metadados exibidos abaixo de cada fonte. + Redefinir formatação + Restaurar formatação padrão de fontes. + Chave de API validada. + Não foi possível validar esta chave de API. + Adicione sua chave de API do MDBList abaixo antes de ativar as classificações. + Necessária para buscar classificações do MDBList + Chave de API + Chave de API + Ativar Classificações do MDBList + Buscar classificações de provedores externos na tela de detalhes de metadados + Chave de API + Provedores externos de classificações + Classificações do MDBList + Ações + Controles de reprodução e salvamento. + Elenco + Lista do elenco principal. + Fundo Cinematográfico + Backdrop desfocado atrás do conteúdo, similar à tela de stream. + Coleção + Coleção relacionada ou rail de franquia. + Comentários + Avaliações do Trakt + Detalhes + Duração, status, lançamento, idioma e informações relacionadas. + Cards de Episódio + Escolha como os episódios são renderizados na tela de metadados. + Horizontal + Cards de linha estilo backdrop + Lista + Cards empilhados com foco em detalhes + Episódios + Temporadas e lista de episódios para séries. + Desfocar Episódios Não Assistidos + Desfocar thumbnails de episódios até serem assistidos para evitar spoilers. + Grupo %1$d + Mais como este + Backdrops de recomendação do TMDB na página de detalhes + Nenhum + Visão Geral + Sinopse, classificações, gêneros e créditos principais. + Produção + Estúdios e emissoras. + APARÊNCIA + SEÇÕES + Grupo de Aba %1$d + Layout de Abas + Agrupe seções em abas como no app de TV. Atribua até 3 seções por grupo de abas. + Trailers + Rail de trailer e atalhos de reprodução. + Notificações estão atualmente desativadas no Nuvio. + Alertas de lançamento de episódios + Agende notificações locais quando um novo episódio de uma série salva se tornar disponível. + Notificações do sistema estão desativadas para o Nuvio. Ative-as para receber alertas e testar notificações. + %1$d alertas de lançamento estão atualmente agendados neste dispositivo. + ALERTAS + TESTE + Enviar Notificação de Teste + Enviando Notificação de Teste... + Enviar uma notificação de teste local para %1$s. + Salve uma série na sua biblioteca primeiro para testar notificações. + Notificação de teste + Comunidade + Veja as pessoas que constroem e apoiam o Nuvio em Mobile, TV e Web. + API de apoiadores não está configurada. Adicione DONATIONS_BASE_URL ao local.properties. + Contribuidores + Apoiadores + Abrir no GitHub + Perfil do GitHub indisponível + Nenhuma mensagem anexada. + Carregando contribuidores... + Carregando apoiadores... + Não foi possível carregar contribuidores + Não foi possível carregar apoiadores + Nenhum contribuidor encontrado. + Nenhum apoiador encontrado. + Não foi possível carregar contribuidores. + Não foi possível carregar apoiadores. + Não foi possível carregar contribuidores no momento. + Não foi possível carregar apoiadores no momento. + %1$d commits no total + Jan + Fev + Mar + Abr + Mai + Jun + Jul + Ago + Set + Out + Nov + Dez + %1$s %2$s, %3$s + Todos os addons instalados + Todos os plugins ativados + Addons Permitidos + Plugins Permitidos + Anime Skip + Client ID do AnimeSkip + Insira seu client ID da API do AnimeSkip. Obtenha um em anime-skip.com. + Ativar Envio de Intro + Mostrar um botão para enviar timestamps de intro/outro para o banco de dados da comunidade. + Chave de API do IntroDB + Insira sua chave de API do IntroDB para enviar timestamps. Necessário para envio. + Também buscar no AnimeSkip por timestamps de pular (requer client ID). + Reproduzir Próximo Episódio Automaticamente + Iniciar próximo episódio automaticamente quando o prompt aparecer. + Apenas decodificadores do dispositivo + Preferir decodificadores do app (FFmpeg) + Preferir decodificadores do dispositivo + Prioridade do Decodificador + Toque fora para fechar + Toque fora para salvar & fechar + %1$d dia + %1$d dias + %1$d hora + %1$d horas + Usar libass para legendas ASS/SSA + Experimental: renderização avançada de ASS/SSA (estilos, posicionamento, animações) + Player Externo + App de Player Externo + Abrir nova reprodução com o app de vídeo padrão do Android ou seletor do sistema. + Abrir nova reprodução com o player instalado selecionado. + Nenhum player externo compatível instalado + Segurar para Velocidade + Segurar para Acelerar + Pressione e segure em qualquer lugar na superfície do player para aumentar temporariamente a velocidade de reprodução. + Padrão regex inválido + Duração do Cache do Último Link + DV7 - Fallback HEVC + Mapear Dolby Vision Profile 7 para HEVC padrão em dispositivos sem suporte de hardware para DV + Minutos de Limite + Fallback quando não houver timestamp de encerramento. + %1$s min + Nenhum item disponível + Não definido + Padrão (arquivo de mídia) + Idioma do dispositivo + Forçada + Nenhuma + Preferir Grupo de Maratona (Próximo Episódio) + Tentar o mesmo perfil de fonte primeiro (mesmo addon/grupo de qualidade) antes das regras normais de reprodução automática. + Reutilizar Grupo de Maratona + Lembrar e reutilizar o último grupo de maratona (binge group) entre sessões (Continuar Assistindo, Detalhes, etc.). + Idioma de Áudio Preferido + Idioma Preferido + Predefinições + Corresponde contra nome/título/descrição/addon/URL da stream. Exemplo: 4K|2160p|Remux + Padrão Regex + Nenhum padrão definido. Exemplo: 4K|2160p|Remux + Qualquer 1080p+ + AVC / x264 + Qualidade BluRay + Dolby Atmos / DTS + Inglês + HDR / Dolby Vision + HEVC / x265 + Sem CAM/TS + Sem REMUX/HDR + 1080p Padrão + 4K / Remux + 720p / Menor + Fontes WEB + Modo de Renderização Libass + Cues Padrão + Effects Canvas + Effects OpenGL + Overlay Canvas + Overlay OpenGL (Recomendado) + Reutilizar Último Link + Reproduzir automaticamente sua última stream funcional para este mesmo filme/episódio enquanto o cache estiver válido + Segundo Idioma de Áudio + Segundo Idioma Preferido + DECODIFICADOR + PRÓXIMO EPISÓDIO + PLAYER + PULAR SEGMENTOS + REPRODUÇÃO AUTOMÁTICA DE STREAM + SELEÇÃO DE STREAM + LEGENDA E ÁUDIO + RENDERIZAÇÃO DE LEGENDA + %1$d selecionado(s) + Overlay de Carregamento + Mostrar tela de carregamento até o primeiro quadro de vídeo aparecer. + Pular Intro + Usar introdb.app para detectar intros e recapitulações. + Escopo de Fonte para Reprodução Automática + Todos os addons instalados + Reprodução automática considera apenas streams vindas de seus addons instalados. + Todas as fontes + Reprodução automática pode usar tanto addons instalados quanto plugins ativados. + Apenas plugins ativados + Reprodução automática considera apenas streams vindas de plugins ativados. + Apenas addons instalados + Reprodução automática considera apenas streams vindas de seus addons instalados. + Seleção Automática de Stream + Reproduzir primeira fonte automaticamente + Reproduzir a primeira fonte disponível automaticamente. + Manual (escolher stream) + Sempre mostrar lista de fontes e permitir que eu escolha. + Reproduzir correspondência regex automaticamente + Reproduzir primeira fonte cujo texto corresponda ao seu padrão regex. + Tempo Limite para Seleção de Stream + Tempo de espera para addons antes de selecionar. + Minutos de Limite + Modo de Limite para Próximo Episódio + Minutos antes do fim + Porcentagem + Porcentagem de Limite + Fallback quando não houver timestamp de encerramento. + %1$s% + Instantâneo + %1$ss + Ilimitado + Reprodução em Túnel + Sincronização de áudio/vídeo em nível de hardware. Pode melhorar a reprodução em alguns dispositivos Android TV + Adicione sua própria chave de API do TMDB abaixo antes de ativar o enriquecimento. + Chave de API + Ativar Enriquecimento do TMDB + Usar o TMDB como fonte de metadados para aprimorar dados de addons + Insira sua chave de API TMDB v3. + Código de idioma + Arte + Imagens de logo e backdrop do TMDB + Informações Básicas + Descrição, gêneros e classificação do TMDB + Coleções + Coleções de filmes do TMDB em ordem de lançamento + Créditos + Elenco com fotos, diretor e roteirista do TMDB + Detalhes + Duração, status, país e idioma do TMDB + Episódios + Títulos de episódios, sinopses, thumbnails e duração do TMDB + Mais como Este + Backdrops de recomendação do TMDB na página de detalhes + Emissoras + Emissoras com logos do TMDB + Produções + Empresas de produção do TMDB + Pôsteres de temporada + Usar pôsteres de temporada do TMDB no seletor de temporadas da tela de metadados para séries. + Trailers + Candidatos a trailer dos vídeos do TMDB para a seção de trailer de detalhes + Chave de API pessoal + Idioma + Idioma dos metadados do TMDB para título, logo e campos ativados + CREDENCIAIS + LOCALIZAÇÃO + MÓDULOS + Enriquecimento do TMDB + Após a aprovação, você será redirecionado de volta automaticamente. + AUTENTICAÇÃO + Comentários + Mostrar avaliações do Trakt nas páginas de metadados + Conectar Trakt + Conectado como %1$s + Usuário Trakt + Desconectar + Falha ao abrir navegador + RECURSOS + Conclua o login do Trakt no seu navegador + Sincronize sua watchlist, progresso de exibição, continuar assistindo, scrobbles e listas pessoais com o Trakt. + Credenciais do Trakt ausentes em local.properties (TRAKT_CLIENT_ID / TRAKT_CLIENT_SECRET). + Abrir Login do Trakt + Suas ações de Salvar agora podem ter como alvo a watchlist e listas pessoais do Trakt. + Faça login com o Trakt para ativar o salvamento baseado em listas e o modo de biblioteca do Trakt. + Fonte da Biblioteca + Escolha qual biblioteca usar para salvar e visualizar sua coleção + Fonte da Biblioteca + Escolha onde salvar e gerenciar seus itens da biblioteca + Trakt + Biblioteca do Nuvio + Biblioteca do Trakt selecionada + Biblioteca do Nuvio selecionada + Progresso de Exibição + Escolha qual fonte de progresso controla retomar e continuar assistindo + Progresso de Exibição + Escolha se retomar e continuar assistindo devem usar Trakt ou Nuvio Sync enquanto o scrobble do Trakt permanece ativo. + Trakt + Nuvio Sync + Fonte de progresso de exibição definida como Trakt + Fonte de progresso de exibição definida como Nuvio Sync + Janela de Continuar Assistindo + Histórico do Trakt considerado para continuar assistindo + Janela de Continuar Assistindo + Escolha quanto da atividade do Trakt deve aparecer em continuar assistindo. + Todo o histórico + %1$d dias + Pontuação do Público + IMDb + Letterboxd + Metacritic + Rotten Tomatoes + TMDB + Trakt + Desconhecido + Âmbar + Carmesim + Esmeralda + Oceano + Rosa + Violeta + Branco + Próximo Episódio + Buscando fonte… + Reproduzindo via %1$s em %2$d… + Thumbnail do próximo episódio + Não exibido + Pular + Pular Intro + Pular Outro + Pular Recapitulação + Nenhuma legenda encontrada + Africâner + Albanês + Amárico + Árabe + Armênio + Azerbaijano + Basco + Bielorrusso + Bengali + Bósnio + Búlgaro + Birmanês + Catalão + Chinês + Chinês (Simplificado) + Chinês (Tradicional) + Croata + Tcheco + Dinamarquês + Holandês + Inglês + Estoniano + Filipino + Finlandês + Francês + Galego + Georgiano + Alemão + Grego + Guzerate + Hebraico + Hindi + Húngaro + Islandês + Indonésio + Irlandês + Italiano + Japonês + Canarim + Cazaque + Khmer + Coreano + Lao + Letão + Lituano + Macedônio + Malaio + Malaiala + Maltês + Marati + Mongol + Nepalês + Norueguês + Persa + Polonês + Português (Portugal) + Português (Brasil) + Punjabi + Romeno + Russo + Sérvio + Cingalês + Eslovaco + Esloveno + Espanhol + Espanhol (América Latina) + Suaíli + Sueco + Tâmil + Télugo + Tailandês + Turco + Ucraniano + Urdu + Uzbeque + Vietnamita + Galês + Zulu + Limpar + Continuar + Ignorar + Instalar + Depois + Não + Atualizar + Sim + Deseja sair do app? + Sair do app + Este catálogo não retornou nenhum item. + Nenhum título encontrado + Verifique sua conexão Wi-Fi ou dados móveis e tente novamente. + Diretor + Falha ao carregar + Mais como Este + Temporadas + Este addon retornou vídeos para a série, mas nenhum incluiu números de temporada ou episódio. + Este addon não forneceu metadados de episódio para esta série. + Episódios ainda não foram publicados por este addon. + Seu dispositivo está online, mas o Nuvio não pôde alcançar os servidores necessários. + Mostrar Menos + Mostrar Mais ▾ + Roteirista + Todos os Gêneros + Catálogo + %1$s • %2$s + O catálogo selecionado falhou ao retornar itens de descoberta. + Não foi possível carregar Descobrir + Addons instalados não expõem catálogos compatíveis com board para descoberta. + Nenhum catálogo de descoberta + O catálogo e filtros selecionados não retornaram nenhum item. + Nenhum título encontrado + Instale e valide pelo menos um addon antes de navegar pelos catálogos de descoberta. + Selecionar Catálogo + Selecionar Gênero + Selecionar Tipo + Tipo + Marcar anteriores como não assistidos + Marcar anteriores como assistidos + Marcar %1$s como não assistido + Marcar %1$s como assistido + Marcar como não assistido + Marcar como assistido + A seguir + %1$s assistido + Instale e valide pelo menos um addon antes de carregar linhas de catálogo na Página Inicial. + Addons instalados não expõem atualmente catálogos compatíveis com board sem extras obrigatórios. + Nenhuma linha da página inicial disponível + Ver Detalhes + Controles de reprodução e salvamento. + Ações + Lista do elenco principal. + Coleção relacionada ou rail de franquia. + Coleção + Seção de comentários do Trakt. + Duração, status, lançamento, idioma e informações relacionadas. + Detalhes + Temporadas e lista de episódios para séries. + Rail de recomendação. + Mais como Este + Sinopse, classificações, gêneros e créditos principais. + Visão Geral + Estúdios e emissoras. + Produção + Rail de trailer e atalhos de reprodução. + Online novamente + Não foi possível alcançar os servidores + Sem conexão com a internet + (idade %1$d) + Nascido %1$s%2$s + Falecido %1$s + Conhecido por: %1$s + Mais Recentes + Não foi possível carregar detalhes para %1$s + Populares + Algo deu errado + Próximos + Apagar + Cancelar + Insira o PIN + Insira o PIN para %1$s + Esqueceu o PIN? + PIN incorreto + Bloqueado. Tente novamente em %1$ds + Opções de avatar aparecerão aqui quando o catálogo carregar. + Avatar: %1$s + Insira uma URL de imagem http:// ou https:// válida. + Escolher um avatar + Escolha um avatar abaixo. + Criar Perfil + URL de avatar personalizada selecionada. + URL de avatar personalizada + Cole um link de imagem, ou deixe vazio para usar o catálogo de avatar integrado. + https://example.com/avatar.png + Todos os dados de "%1$s" serão excluídos permanentemente. + Excluir Perfil + Adicionar Perfil + Editar Perfil + Insira o PIN atual + Insira o novo PIN + Perfil %1$d + Carregando avatares... + Gerenciar Perfis + Nome do perfil + Novo perfil + Addons primários desativados + Addons primários ativados + Remover PIN para %1$s + Remover Bloqueio por PIN + Salvando... + Segurança + Adicione um PIN se quiser que este perfil seja bloqueado antes de alternar para ele. + Este perfil está protegido com um PIN. + Selecione um avatar para este perfil. + Definir Bloqueio por PIN + Perfil sem nome + Usar Addons Primários + Compartilhar a configuração de addon do perfil principal em vez de gerenciar uma lista separada. + Quem está assistindo? + Baixado + Retomar + Scrapers ativos + Verificando mais addons… + Copiar link da stream + Baixar arquivo + Abrir em player externo + Abrir em player interno + Os addons de stream instalados falharam ao retornar uma resposta de stream válida. + Não foi possível carregar streams + Instale um addon primeiro para carregar streams para este título. + Seus addons instalados não fornecem streams para este tipo de título. + Nenhum addon de stream disponível + Nenhum dos seus addons instalados retornou streams para este título. + T%1$d E%2$d + Episódio + T%1$dE%2$d - %3$s + Buscando… + Buscando fonte… + Buscando streams… + Link da stream copiado + Nenhum link direto de stream disponível + Nenhum metadado disponível + Atualizar streams + Retomar de %1$d% + Retomar de %1$s + TAMANHO %1$s + Streams de torrent não são suportados + Adicione uma chave de API do Debrid nas Configurações. + Este resultado do Debrid expirou. Atualizando streams. + Não foi possível resolver esta stream do Debrid. + Não foi possível abrir o player externo + Escolha um player externo nas configurações primeiro + Nenhum player externo está disponível + Fechar trailer + Não foi possível reproduzir o trailer + Falha ao carregar listas do Trakt + Falha ao atualizar listas do Trakt + %1$s • %2$s + Falha na verificação de atualização + Download falhou + Baixando %1$d% + Não foi possível iniciar a instalação + Você está usando a versão mais recente. + Ative instalações de apps para o Nuvio, então volte e continue. + Baixando atualização... + Nenhuma atualização encontrada. + Uma nova versão está pronta para instalar. + Atualizações no app não estão disponíveis nesta build. + Preparando download + Notas de lançamento + Permitir instalações para continuar + Atualização disponível + Status da atualização + Esse addon já está instalado. + Insira uma URL de addon válida + Não foi possível carregar o manifesto + Nuvio + Falha na exclusão da conta + Falha no login + Falha ao sair + Falha no cadastro + Não foi possível carregar itens do catálogo. + A Seguir + A Seguir • T%1$dE%2$d + Logo de %1$s + Falha ao carregar comentários + Não foi possível carregar detalhes de nenhum addon. + Emissoras + Nenhum addon fornece metadados para este conteúdo. + Download falhou + Mostra progresso e controles de download em tempo real. + Downloads + Download concluído + Baixando %1$s • %2$s + Baixando %1$s • %2$s / %3$s + Download falhou + Pausado %1$s + Remover + Remover %1$s de %2$s? + Remover %1$s da sua biblioteca? + Remover da Biblioteca? + Filme + Alertas quando um novo episódio de uma série salva é lançado. + Pré-visualização de alerta de lançamento de episódio. + Falha ao enviar notificação de teste. + Notificação de teste enviada para %1$s. + Não foi possível reproduzir esta stream. + O PIN deste perfil mudou. Conecte-se uma vez para atualizar o bloqueio neste dispositivo. + Não foi possível remover o bloqueio por PIN. Tente novamente. + Conecte-se à internet para remover o bloqueio por PIN. + Este PIN ainda não pode ser verificado offline neste dispositivo. Conecte-se uma vez e desbloqueie online primeiro. + Não foi possível definir o PIN. Tente novamente. + Conecte-se à internet para definir um PIN. + Este perfil usa addons primários. + Falha ao carregar %1$s + Stream + Incorporada + Autorização negada + Conclua o login do Trakt no seu navegador + Callback do Trakt inválido + Estado de callback do Trakt inválido + Resposta de token do Trakt inválida + Falha ao carregar biblioteca do Trakt + Lista %1$d + Trakt não retornou um código de autorização + Credenciais do Trakt ausentes + Falha ao carregar progresso do Trakt + Falha ao concluir login do Trakt + Usuário Trakt + Watchlist + Trailer + Desconhecido + Addon + Salvo + Reproduzir %1$s + Retomar %1$s + JSON está vazio. + Coleção %1$d tem id em branco. + Coleção '%1$s' tem título em branco. + Pasta %1$d em '%2$s' tem id em branco. + Pasta '%1$s' em '%2$s' tem título em branco. + Fonte %1$d na pasta '%2$s' tem campos em branco. + Fonte %1$d na pasta '%2$s' está sem ID de lista do Trakt. + JSON inválido: %1$s + Addon não encontrado: %1$s + Janeiro + Fevereiro + Março + Abril + Maio + Junho + Julho + Agosto + Setembro + Outubro + Novembro + Dezembro + Jan + Fev + Mar + Abr + Mai + Jun + Jul + Ago + Set + Out + Nov + Dez + Empresa de Produção + Emissora + Não foi possível carregar %1$s + Populares + Recentes + %1$s • %2$s + Mais Bem Avaliados + Classificação + Detalhes do Filme + Idioma Original + País de Origem + Informações de Lançamento + Duração + Pôsteres + Texto + Mostrar Detalhes + Status + Vídeos + ARQUIVO + Nenhum link direto de stream disponível + Download anterior substituído + Download iniciado + Formato de stream não suportado para downloads + Corpo da resposta vazio + Solicitação falhou com HTTP %1$d + Sistema de download não está inicializado + Solicitação de download falhou + %1$s - %2$s + Títulos salvos aparecerão aqui após você tocar em Salvar em uma tela de detalhes. + Sua biblioteca está vazia + Não foi possível carregar a biblioteca + Outro + Biblioteca + Conecte o Trakt e salve títulos na sua watchlist ou listas pessoais. + Sua biblioteca do Trakt está vazia + Não foi possível carregar biblioteca do Trakt + Biblioteca do Trakt + Anime + Canais + Filmes + Séries + TV + %1$s já está disponível + %1$s • %2$s já está disponível + Um novo episódio já está disponível + %1$s já está disponível + Lançamentos de Episódios + Álcool/Drogas + Assustador + Nudez + Linguagem Obscena + Leve + Moderado + Grave + Violência + Criador + Diretor + Roteirista + Pontuação do Público + Nenhuma stream de trailer reproduzível encontrada. + Temporada %1$d - %2$s + B + KB + MB + GB + From 3a87ecce0f64102588cf9d2f94f35e3059aedeb1 Mon Sep 17 00:00:00 2001 From: WhiteGiso Date: Sat, 6 Jun 2026 00:04:26 +0200 Subject: [PATCH 4/9] feat(ios): rapid profile switch for liquid glass --- composeApp/proguard-rules.pro | 4 + .../app/core/ui/NativeTabBridge.android.kt | 9 + .../composeResources/values-cs/strings.xml | 2 +- .../composeResources/values-fr/strings.xml | 2 +- .../composeResources/values-id/strings.xml | 2 +- .../composeResources/values-it/strings.xml | 2 +- .../composeResources/values-nb/strings.xml | 2 +- .../composeResources/values-pl/strings.xml | 2 +- .../composeResources/values-pt/strings.xml | 2 +- .../composeResources/values-tr/strings.xml | 2 +- .../composeResources/values/strings.xml | 2 +- .../commonMain/kotlin/com/nuvio/app/App.kt | 54 ++++- .../com/nuvio/app/core/ui/NativeTabBridge.kt | 28 +++ .../features/profiles/ProfileSwitcherTab.kt | 185 +++++++++++++++++- .../nuvio/app/core/ui/NativeTabBridge.ios.kt | 25 +++ iosApp/Configuration/Version.xcconfig | 4 +- iosApp/iosApp/ContentView.swift | 151 +++++++++++++- iosApp/iosApp/Player/MPVPlayerBridge.swift | 6 + 18 files changed, 463 insertions(+), 21 deletions(-) diff --git a/composeApp/proguard-rules.pro b/composeApp/proguard-rules.pro index bbe05c0c8..29e2b9b63 100644 --- a/composeApp/proguard-rules.pro +++ b/composeApp/proguard-rules.pro @@ -29,6 +29,10 @@ -keep class com.nuvio.app.features.streams.StreamsScreenKt { *; } -keep class com.nuvio.app.features.streams.StreamsScreenKt$* { *; } +# Avoid R8 producing verifier-invalid bytecode for the large player composable. +-keep class com.nuvio.app.features.player.PlayerScreenKt { *; } +-keep class com.nuvio.app.features.player.PlayerScreenKt$* { *; } + # QuickJS plugin runtime is dynamic; keep runtime and app plugin classes. -keep class com.dokar.quickjs.** { *; } -keep class com.nuvio.app.features.plugins.** { *; } diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt index c7c556c50..900c3489b 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt @@ -10,9 +10,18 @@ internal actual fun publishNativeSelectedTab(tabName: String) = Unit internal actual fun publishNativeTabAccentColor(hexColor: String) = Unit +internal actual fun publishNativeTabTitles( + home: String, + search: String, + library: String, + profile: String, +) = Unit + internal actual fun publishNativeProfileTabIcon( name: String?, avatarColorHex: String?, avatarImageUrl: String?, avatarBackgroundColorHex: String?, ) = Unit + +internal actual fun notifyNativeProfileSwitcherPopupDismissed() = Unit diff --git a/composeApp/src/commonMain/composeResources/values-cs/strings.xml b/composeApp/src/commonMain/composeResources/values-cs/strings.xml index 5e186e5f4..298527b02 100644 --- a/composeApp/src/commonMain/composeResources/values-cs/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-cs/strings.xml @@ -453,7 +453,7 @@ Vyberte jazyk Nastavení pro sekci Pokračovat ve sledování. Tekuté sklo (Liquid Glass) - Použít nativní lištu panelů na iPhonu v iOS 26 a novějším. Okamžité přepínání profilů z lišty panelů není při zapnutí dostupné. + Použít nativní lištu panelů na iPhonu v iOS 26 a novějším. Vyladit šířku karty a poloměr rohů. ZOBRAZENÍ DOMŮ diff --git a/composeApp/src/commonMain/composeResources/values-fr/strings.xml b/composeApp/src/commonMain/composeResources/values-fr/strings.xml index a09f31754..95e83d86a 100644 --- a/composeApp/src/commonMain/composeResources/values-fr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-fr/strings.xml @@ -506,7 +506,7 @@ Choisir la langue Afficher, masquer et ajuster le bandeau Continuer à regarder. Liquid Glass - Utilise la barre d’onglets native iPhone sur iOS 26 et versions ultérieures. Le changement instantané de profil depuis la barre d’onglets n’est pas disponible quand cette option est activée. + Utilise la barre d’onglets native iPhone sur iOS 26 et versions ultérieures. Ajuste la largeur des cartes et le rayon des coins. AFFICHAGE ACCUEIL diff --git a/composeApp/src/commonMain/composeResources/values-id/strings.xml b/composeApp/src/commonMain/composeResources/values-id/strings.xml index e442e971d..35a923d9f 100644 --- a/composeApp/src/commonMain/composeResources/values-id/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-id/strings.xml @@ -508,7 +508,7 @@ Pilih Bahasa Pengaturan untuk bagian Lanjutkan Menonton. Liquid Glass - Gunakan tab bar iPhone asli di iOS 26 dan yang lebih baru. Pergantian profil instan dari tab bar tidak tersedia saat ini aktif. + Gunakan tab bar iPhone asli di iOS 26 dan yang lebih baru. Sesuaikan lebar kartu dan radius sudut. TAMPILAN BERANDA diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml index c72739585..d96e8195f 100644 --- a/composeApp/src/commonMain/composeResources/values-it/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml @@ -1240,7 +1240,7 @@ Utilizzato per la riproduzione sulle build Android. Rilasciato sotto la licenza Apache License, Versione 2.0. Liquid Glass - Usa la barra dei pannelli nativa dell'iPhone su iOS 26 e versioni successive. Il cambio rapido del profilo dalla barra dei pannelli non è disponibile quando questa opzione è attiva. + Usa la barra dei pannelli nativa dell'iPhone su iOS 26 e versioni successive. Nascondi contenuti non rilasciati Nascondi i film e le serie TV che non sono ancora stati rilasciati. Nascondi sottolineatura catalogo diff --git a/composeApp/src/commonMain/composeResources/values-nb/strings.xml b/composeApp/src/commonMain/composeResources/values-nb/strings.xml index 253a32cc2..ad7cea8c3 100644 --- a/composeApp/src/commonMain/composeResources/values-nb/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-nb/strings.xml @@ -492,7 +492,7 @@ Velg språk Innstillinger for Fortsett å se-seksjonen. Liquid Glass - Bruk den innebygde iPhone-fanen i iOS 26 og nyere. Umiddelbar profilbytte fra fanelinjen er ikke tilgjengelig mens dette er på. + Bruk den innebygde iPhone-fanen i iOS 26 og nyere. Juster kortbredde og hjørneradius. VISNING HJEM diff --git a/composeApp/src/commonMain/composeResources/values-pl/strings.xml b/composeApp/src/commonMain/composeResources/values-pl/strings.xml index 3e487c29c..1374aebbe 100644 --- a/composeApp/src/commonMain/composeResources/values-pl/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pl/strings.xml @@ -508,7 +508,7 @@ Wybierz język Pokaż, ukryj i stylizuj półkę Kontynuuj oglądanie. Liquid Glass - Użyj natywnego paska kart iPhone na iOS 26 i nowszych. Szybkie przełączanie profili z paska kart jest niedostępne, gdy ta opcja jest włączona. + Użyj natywnego paska kart iPhone na iOS 26 i nowszych. Dostosuj szerokość i zaokrąglenie rogów kart plakatów. WYŚWIETLANIE EKRAN GŁÓWNY diff --git a/composeApp/src/commonMain/composeResources/values-pt/strings.xml b/composeApp/src/commonMain/composeResources/values-pt/strings.xml index 725e8cc2e..74c572aee 100644 --- a/composeApp/src/commonMain/composeResources/values-pt/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-pt/strings.xml @@ -498,7 +498,7 @@ Escolher idioma Definições para a secção Continuar a Ver. Liquid Glass - Usa a barra de separadores nativa do iPhone no iOS 26 e posterior. A mudança instantânea de perfil a partir da barra de separadores fica indisponível enquanto isto estiver ativo. + Usa a barra de separadores nativa do iPhone no iOS 26 e posterior. Ajusta a largura dos cartões e o raio dos cantos. ECRÃ ECRÃ INICIAL diff --git a/composeApp/src/commonMain/composeResources/values-tr/strings.xml b/composeApp/src/commonMain/composeResources/values-tr/strings.xml index 9d54c44b1..1340bde98 100644 --- a/composeApp/src/commonMain/composeResources/values-tr/strings.xml +++ b/composeApp/src/commonMain/composeResources/values-tr/strings.xml @@ -498,7 +498,7 @@ Dil seç İzlemeye Devam Et rafını göster, gizle ve stilini ayarla. Liquid Glass - iOS 26 ve sonrasında yerel iPhone sekme çubuğunu kullan. Bu özellik açıkken sekme çubuğundan anlık profil geçişi yapılamaz. + iOS 26 ve sonrasında yerel iPhone sekme çubuğunu kullan. Uygulama genelindeki poster kartlarının ortak genişliğini ve köşe yuvarlaklığını ayarla. EKRAN ANA SAYFA diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index fd8226bfc..5eb9014a5 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -526,7 +526,7 @@ Choose Language Settings for the Continue Watching section. Liquid Glass - Use the native iPhone tab bar on iOS 26 and later. Instant profile switching from the tab bar is unavailable while this is on. + Use the native iPhone tab bar on iOS 26 and later. Tune card width and corner radius. DISPLAY HOME diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 444573b93..36fd8e3a4 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -158,6 +158,7 @@ import com.nuvio.app.features.player.sanitizePlaybackHeaders import com.nuvio.app.features.player.sanitizePlaybackResponseHeaders import com.nuvio.app.features.profiles.AvatarRepository import com.nuvio.app.features.profiles.NuvioProfile +import com.nuvio.app.features.profiles.NativeProfileSwitcherPopup import com.nuvio.app.features.profiles.ProfileEditScreen import com.nuvio.app.features.profiles.ProfileRepository import com.nuvio.app.features.profiles.ProfileSelectionScreen @@ -592,6 +593,7 @@ private fun MainAppContent( val searchScrollToTopRequests = remember { MutableSharedFlow(extraBufferCapacity = 1) } val libraryScrollToTopRequests = remember { MutableSharedFlow(extraBufferCapacity = 1) } val settingsRootActionRequests = remember { MutableSharedFlow(extraBufferCapacity = 1) } + var nativeProfileSwitcherVisible by remember { mutableStateOf(false) } val currentBackStackEntry by navController.currentBackStackEntryAsState() val liquidGlassNativeTabBarEnabled by remember { ThemeSettingsRepository.liquidGlassNativeTabBarEnabled @@ -644,6 +646,10 @@ private fun MainAppContent( val cloudLibraryPlayFailedText = stringResource(Res.string.cloud_library_play_failed) val cloudLibraryPlayDisabledText = stringResource(Res.string.cloud_library_play_disabled) val cloudLibraryPlayNotConnectedText = stringResource(Res.string.cloud_library_play_not_connected) + val nativeTabHomeTitle = stringResource(Res.string.compose_nav_home) + val nativeTabSearchTitle = stringResource(Res.string.compose_nav_search) + val nativeTabLibraryTitle = stringResource(Res.string.compose_nav_library) + val nativeTabProfileTitle = stringResource(Res.string.compose_nav_profile) val isTraktLibrarySource = libraryUiState.sourceMode == LibrarySourceMode.TRAKT var initialHomeReady by rememberSaveable { mutableStateOf(false) } var offlineLaunchRouteHandled by rememberSaveable { mutableStateOf(false) } @@ -675,6 +681,28 @@ private fun MainAppContent( } } + LaunchedEffect(liquidGlassNativeTabBarSupported, liquidGlassNativeTabBarEnabled) { + NativeTabBridge.profileTabLongPresses.collectLatest { + if (liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled) { + nativeProfileSwitcherVisible = true + } + } + } + + LaunchedEffect( + nativeTabHomeTitle, + nativeTabSearchTitle, + nativeTabLibraryTitle, + nativeTabProfileTitle, + ) { + NativeTabBridge.publishTabTitles( + home = nativeTabHomeTitle, + search = nativeTabSearchTitle, + library = nativeTabLibraryTitle, + profile = nativeTabProfileTitle, + ) + } + LaunchedEffect(selectedTab) { NativeTabBridge.publishSelectedTab(selectedTab.toNativeNavigationTab()) if (selectedTab != AppScreenTab.Search) { @@ -682,16 +710,20 @@ private fun MainAppContent( } } + var profileSwitchLoading by remember { mutableStateOf(false) } + DisposableEffect( navController, liquidGlassNativeTabBarSupported, liquidGlassNativeTabBarEnabled, initialHomeReady, + profileSwitchLoading, ) { fun publishNativeTabVisibilityForCurrentRoute() { val visible = liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady && + !profileSwitchLoading && navController.currentDestination?.hasRoute() == true NativeTabBridge.publishTabBarVisible(visible) } @@ -801,7 +833,6 @@ private fun MainAppContent( SyncManager.requestForegroundPull(activeProfileId, force = true) } } - var profileSwitchLoading by remember { mutableStateOf(false) } var resumePromptItem by remember { mutableStateOf(null) } var lastExternalPlayerLaunch by remember { mutableStateOf(null) } val streamLaunchIdsPreservedForPlayerReturn = remember { mutableSetOf() } @@ -1283,7 +1314,9 @@ private fun MainAppContent( liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady val tabsRouteActive = currentBackStackEntry?.destination?.hasRoute() == true val onProfileSelected: (NuvioProfile) -> Unit = { profile -> + nativeProfileSwitcherVisible = false profileSwitchLoading = true + NativeTabBridge.publishTabBarVisible(false) selectedTab = AppScreenTab.Home ProfileRepository.selectProfile(profile.profileIndex) com.nuvio.app.core.sync.SyncManager.pullAllForProfile(profile.profileIndex) @@ -1436,7 +1469,24 @@ private fun MainAppContent( selectedTab = selectedTab, onTabSelected = ::handleRootTabClick, onProfileSelected = onProfileSelected, - onAddProfileRequested = onSwitchProfile, + onAddProfileRequested = { + nativeProfileSwitcherVisible = false + onSwitchProfile() + }, + ) + } + + if (!isTabletLayout && useNativeBottomTabs && tabsRouteActive) { + NativeProfileSwitcherPopup( + visible = nativeProfileSwitcherVisible, + isSwitchingProfile = profileSwitchLoading, + onDismissRequest = { nativeProfileSwitcherVisible = false }, + onProfileSelected = onProfileSelected, + onAddProfileRequested = { + nativeProfileSwitcherVisible = false + onSwitchProfile() + }, + modifier = Modifier.fillMaxSize(), ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt index aa426d022..18299c1fa 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt @@ -20,11 +20,17 @@ internal enum class NativeNavigationTab { internal object NativeTabBridge { private val _requestedTabs = MutableSharedFlow(extraBufferCapacity = 1) val requestedTabs: SharedFlow = _requestedTabs.asSharedFlow() + private val _profileTabLongPresses = MutableSharedFlow(extraBufferCapacity = 1) + val profileTabLongPresses: SharedFlow = _profileTabLongPresses.asSharedFlow() fun requestTab(tabName: String) { _requestedTabs.tryEmit(NativeNavigationTab.fromName(tabName)) } + fun requestProfileTabLongPress() { + _profileTabLongPresses.tryEmit(Unit) + } + fun publishSelectedTab(tab: NativeNavigationTab) { publishNativeSelectedTab(tab.name) } @@ -41,6 +47,15 @@ internal object NativeTabBridge { publishNativeTabAccentColor(hexColor) } + fun publishTabTitles( + home: String, + search: String, + library: String, + profile: String, + ) { + publishNativeTabTitles(home, search, library, profile) + } + fun publishProfileTabIcon( name: String?, avatarColorHex: String?, @@ -60,6 +75,10 @@ fun nativeTabSelect(tabName: String) { NativeTabBridge.requestTab(tabName) } +fun nativeProfileTabLongPress() { + NativeTabBridge.requestProfileTabLongPress() +} + internal expect fun isLiquidGlassNativeTabBarSupported(): Boolean internal expect fun publishLiquidGlassNativeTabBarEnabled(enabled: Boolean) @@ -70,9 +89,18 @@ internal expect fun publishNativeSelectedTab(tabName: String) internal expect fun publishNativeTabAccentColor(hexColor: String) +internal expect fun publishNativeTabTitles( + home: String, + search: String, + library: String, + profile: String, +) + internal expect fun publishNativeProfileTabIcon( name: String?, avatarColorHex: String?, avatarImageUrl: String?, avatarBackgroundColorHex: String?, ) + +internal expect fun notifyNativeProfileSwitcherPopupDismissed() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index a399c8220..cf477d8e8 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -18,8 +18,10 @@ import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -70,6 +72,7 @@ import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.nuvio.app.isIos +import com.nuvio.app.core.ui.notifyNativeProfileSwitcherPopupDismissed import kotlinx.coroutines.delay import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* @@ -139,8 +142,8 @@ fun ProfileSwitcherTab( if (profile.pinEnabled) { pinProfile = profile } else { - onProfileSelected(profile) showPopup = false + onProfileSelected(profile) } } @@ -335,6 +338,186 @@ fun ProfileSwitcherTab( } } +@Composable +fun NativeProfileSwitcherPopup( + visible: Boolean, + isSwitchingProfile: Boolean, + onDismissRequest: () -> Unit, + onProfileSelected: (NuvioProfile) -> Unit, + onAddProfileRequested: () -> Unit, + modifier: Modifier = Modifier, +) { + val profileState by ProfileRepository.state.collectAsStateWithLifecycle() + val activeProfile = profileState.activeProfile + val profiles = profileState.profiles + val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle() + val haptic = LocalHapticFeedback.current + val density = LocalDensity.current + + var showPopup by remember { mutableStateOf(false) } + var popupVisible by remember { mutableStateOf(false) } + var pinProfile by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + AvatarRepository.fetchAvatars() + AvatarRepository.refreshAvatars() + } + + LaunchedEffect(visible, isSwitchingProfile, profiles.isNotEmpty()) { + if (visible && !isSwitchingProfile) { + if (profiles.isNotEmpty()) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + showPopup = true + } else { + onDismissRequest() + showPopup = false + } + } else { + showPopup = false + } + } + + fun chooseProfile(profile: NuvioProfile) { + if (profile.pinEnabled) { + pinProfile = profile + } else { + showPopup = false + onDismissRequest() + onProfileSelected(profile) + } + } + + val popupAlpha = remember { Animatable(0f) } + val popupScale = remember { Animatable(0.5f) } + val popupTranslateY = remember { Animatable(40f) } + + LaunchedEffect(showPopup) { + if (showPopup) { + popupVisible = true + launch { popupAlpha.animateTo(1f, tween(220, easing = FastOutSlowInEasing)) } + launch { + popupScale.animateTo( + 1f, + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium, + ), + ) + } + launch { + popupTranslateY.animateTo( + 0f, + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium, + ), + ) + } + } else if (popupVisible) { + notifyNativeProfileSwitcherPopupDismissed() + launch { popupAlpha.animateTo(0f, tween(180, easing = FastOutSlowInEasing)) } + launch { popupScale.animateTo(0.85f, tween(200, easing = FastOutSlowInEasing)) } + launch { + popupTranslateY.animateTo(30f, tween(200, easing = FastOutSlowInEasing)) + popupVisible = false + pinProfile = null + } + } + } + + BoxWithConstraints(modifier = modifier) { + val anchorWidth = maxWidth / 4 + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .fillMaxHeight() + .width(anchorWidth), + ) { + if (popupVisible && profiles.isNotEmpty() && !isSwitchingProfile) { + Popup( + alignment = Alignment.BottomCenter, + offset = IntOffset(0, with(density) { -84.dp.roundToPx() }), + properties = PopupProperties(focusable = true), + onDismissRequest = onDismissRequest, + ) { + Box( + modifier = Modifier + .imePadding() + .graphicsLayer { + alpha = popupAlpha.value + scaleX = popupScale.value + scaleY = popupScale.value + translationY = popupTranslateY.value + } + .shadow(16.dp, RoundedCornerShape(28.dp)) + .background( + MaterialTheme.colorScheme.surfaceContainerHigh, + RoundedCornerShape(28.dp), + ) + .padding(16.dp), + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top, + ) { + profiles.forEachIndexed { index, profile -> + PopupProfileBubble( + profile = profile, + avatars = avatars, + isActive = profile.profileIndex == activeProfile?.profileIndex, + isSelected = pinProfile?.profileIndex == profile.profileIndex, + delayMs = index * 50, + onBoundsChanged = {}, + onClick = { chooseProfile(profile) }, + ) + } + + if (profiles.size < 4) { + PopupAddProfileBubble( + delayMs = profiles.size * 50, + onClick = { + showPopup = false + onDismissRequest() + onAddProfileRequested() + }, + ) + } + } + + AnimatedVisibility( + visible = pinProfile != null, + enter = expandVertically( + animationSpec = spring( + dampingRatio = Spring.DampingRatioLowBouncy, + stiffness = Spring.StiffnessMediumLow, + ), + ) + fadeIn(tween(200)), + exit = shrinkVertically(tween(150)) + fadeOut(tween(100)), + ) { + pinProfile?.let { profile -> + InlinePinEntry( + profileName = profile.name, + onVerified = { + showPopup = false + onDismissRequest() + onProfileSelected(profile) + }, + onCancel = { pinProfile = null }, + verifyPin = { pin -> + ProfileRepository.verifyPin(profile.profileIndex, pin) + }, + ) + } + } + } + } + } + } + } + } +} + @Composable private fun PopupAddProfileBubble( delayMs: Int, diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt index 1b72da7c2..5fad29aab 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt @@ -9,10 +9,15 @@ private const val liquidGlassNativeTabBarEnabledKey = "NuvioLiquidGlassNativeTab private const val nativeTabBarVisibleKey = "NuvioNativeTabBarVisible" private const val nativeSelectedTabKey = "NuvioNativeSelectedTab" private const val nativeTabAccentColorKey = "NuvioNativeTabAccentColor" +private const val nativeTabTitleHomeKey = "NuvioNativeTabTitleHome" +private const val nativeTabTitleSearchKey = "NuvioNativeTabTitleSearch" +private const val nativeTabTitleLibraryKey = "NuvioNativeTabTitleLibrary" +private const val nativeTabTitleProfileKey = "NuvioNativeTabTitleProfile" private const val nativeProfileNameKey = "NuvioNativeProfileName" private const val nativeProfileAvatarColorKey = "NuvioNativeProfileAvatarColor" private const val nativeProfileAvatarUrlKey = "NuvioNativeProfileAvatarURL" private const val nativeProfileAvatarBackgroundColorKey = "NuvioNativeProfileAvatarBackgroundColor" +private const val nativeProfileSwitcherPopupDismissedNotification = "NuvioNativeProfileSwitcherPopupDismissed" private const val nativeTabChromeDidChangeNotification = "NuvioNativeTabChromeDidChange" internal actual fun isLiquidGlassNativeTabBarSupported(): Boolean { @@ -38,6 +43,19 @@ internal actual fun publishNativeTabAccentColor(hexColor: String) { notifyNativeTabChromeChanged() } +internal actual fun publishNativeTabTitles( + home: String, + search: String, + library: String, + profile: String, +) { + publishString(nativeTabTitleHomeKey, home) + publishString(nativeTabTitleSearchKey, search) + publishString(nativeTabTitleLibraryKey, library) + publishString(nativeTabTitleProfileKey, profile) + notifyNativeTabChromeChanged() +} + internal actual fun publishNativeProfileTabIcon( name: String?, avatarColorHex: String?, @@ -51,6 +69,13 @@ internal actual fun publishNativeProfileTabIcon( notifyNativeTabChromeChanged() } +internal actual fun notifyNativeProfileSwitcherPopupDismissed() { + NSNotificationCenter.defaultCenter.postNotificationName( + nativeProfileSwitcherPopupDismissedNotification, + null, + ) +} + private fun publishBool(key: String, value: Boolean) { NSUserDefaults.standardUserDefaults.setBool(value, forKey = key) notifyNativeTabChromeChanged() diff --git a/iosApp/Configuration/Version.xcconfig b/iosApp/Configuration/Version.xcconfig index 1a9a5661d..92fd04ca2 100644 --- a/iosApp/Configuration/Version.xcconfig +++ b/iosApp/Configuration/Version.xcconfig @@ -1,3 +1,3 @@ -CURRENT_PROJECT_VERSION=73 -MARKETING_VERSION=0.2.2 +CURRENT_PROJECT_VERSION=74 +MARKETING_VERSION=0.2.3 diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index 14f5664a0..ac5db8145 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -253,7 +253,7 @@ private enum NuvioNativeTabIcon { } } -final class RootComposeViewController: UIViewController, UITabBarDelegate { +final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGestureRecognizerDelegate { private enum NativeTab: String, CaseIterable { case home = "Home" case search = "Search" @@ -269,7 +269,16 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { } } - var title: String { + var titleKey: String { + switch self { + case .home: return "NuvioNativeTabTitleHome" + case .search: return "NuvioNativeTabTitleSearch" + case .library: return "NuvioNativeTabTitleLibrary" + case .settings: return "NuvioNativeTabTitleProfile" + } + } + + var fallbackTitle: String { switch self { case .home: return "Home" case .search: return "Search" @@ -278,6 +287,10 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { } } + func localizedTitle(defaults: UserDefaults = .standard) -> String { + defaults.string(forKey: titleKey)?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? fallbackTitle + } + var iconImage: UIImage { switch self { case .home: return NuvioNativeTabIcon.home @@ -301,6 +314,7 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { private static let nativeProfileAvatarColorKey = "NuvioNativeProfileAvatarColor" private static let nativeProfileAvatarURLKey = "NuvioNativeProfileAvatarURL" private static let nativeProfileAvatarBackgroundColorKey = "NuvioNativeProfileAvatarBackgroundColor" + private static let nativeProfileSwitcherPopupDismissedNotification = Notification.Name("NuvioNativeProfileSwitcherPopupDismissed") private static let nativeTabChromeDidChangeNotification = Notification.Name("NuvioNativeTabChromeDidChange") private let contentController: UIViewController @@ -309,6 +323,9 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { private var tabBarHeightConstraint: NSLayoutConstraint? private var userDefaultsObserver: NSObjectProtocol? private var tabChromeObserver: NSObjectProtocol? + private var profileSwitcherPopupObserver: NSObjectProtocol? + private var profileLongPressRecognizer: UILongPressGestureRecognizer? + private var suppressNextProfileSelection = false private var profileAvatarImageURL: String? private var profileAvatarImageTask: URLSessionDataTask? private var profileAvatarImage: UIImage? @@ -355,6 +372,9 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { if let tabChromeObserver { NotificationCenter.default.removeObserver(tabChromeObserver) } + if let profileSwitcherPopupObserver { + NotificationCenter.default.removeObserver(profileSwitcherPopupObserver) + } profileAvatarImageTask?.cancel() } @@ -365,8 +385,26 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let tab = NativeTab(tag: item.tag) else { return } - UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey) - NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue) + if tab == .settings && suppressNextProfileSelection { + suppressNextProfileSelection = false + restoreNativeTabFocus(to: currentNativeSelectedTab) + return + } + selectNativeTab(tab) + } + + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + gestureRecognizer === profileLongPressRecognizer + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + if gestureRecognizer === profileLongPressRecognizer { + return nativeTab(at: touch.location(in: tabBar)) == .settings + } + return true } override var childForHomeIndicatorAutoHidden: UIViewController? { @@ -441,13 +479,19 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { tabBar.translatesAutoresizingMaskIntoConstraints = false tabBar.items = NativeTab.allCases.map { tab in let item = UITabBarItem( - title: tab.title, + title: tab.localizedTitle(), image: tab.iconImage, selectedImage: tab.iconImage ) item.tag = tab.tag return item } + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleNativeProfileTabLongPress(_:))) + longPressRecognizer.delegate = self + longPressRecognizer.minimumPressDuration = 0.45 + longPressRecognizer.cancelsTouchesInView = true + tabBar.addGestureRecognizer(longPressRecognizer) + profileLongPressRecognizer = longPressRecognizer tabBar.selectedItem = tabBar.items?.first applyNativeTabBarAppearance() tabBar.alpha = 0 @@ -480,6 +524,14 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { ) { [weak self] _ in self?.syncNativeTabChrome(animated: true) } + + profileSwitcherPopupObserver = NotificationCenter.default.addObserver( + forName: Self.nativeProfileSwitcherPopupDismissedNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.suppressNextProfileSelection = false + } } private var tabBarHeight: CGFloat { @@ -525,9 +577,70 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { } private func syncSelectedNativeTab() { + tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == currentNativeSelectedTab.tag }) + } + + @objc private func handleNativeProfileTabLongPress(_ recognizer: UILongPressGestureRecognizer) { + guard recognizer.state == .began else { return } + + suppressNextProfileSelection = true + let tabToRestore = currentNativeSelectedTab + cancelNativeTabTracking() + DispatchQueue.main.async { [weak self] in + self?.restoreNativeTabFocus(to: tabToRestore) + NativeTabBridgeKt.nativeProfileTabLongPress() + } + } + + private var currentNativeSelectedTab: NativeTab { let rawValue = UserDefaults.standard.string(forKey: Self.nativeSelectedTabKey) ?? NativeTab.home.rawValue - let selectedTab = NativeTab(rawValue: rawValue) ?? .home - tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == selectedTab.tag }) + return NativeTab(rawValue: rawValue) ?? .home + } + + private func nativeTab(at point: CGPoint) -> NativeTab? { + guard tabBar.bounds.contains(point), tabBar.bounds.width > 0 else { return nil } + let tabs = NativeTab.allCases + let rawIndex = Int((point.x / tabBar.bounds.width) * CGFloat(tabs.count)) + let clampedIndex = min(max(rawIndex, 0), tabs.count - 1) + return tabs[clampedIndex] + } + + private func selectNativeTab(_ tab: NativeTab) { + tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == tab.tag }) + UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey) + NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue) + } + + private func restoreNativeTabFocus(to tab: NativeTab) { + guard let item = tabBar.items?.first(where: { $0.tag == tab.tag }) else { return } + UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey) + NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue) + + tabBar.cancelControlTrackingRecursively() + + let restoreDuration: TimeInterval = 0.24 + UIView.animate( + withDuration: restoreDuration, + delay: 0, + options: [.beginFromCurrentState, .allowUserInteraction, .curveEaseInOut] + ) { + self.tabBar.selectedItem = item + self.tabBar.layoutIfNeeded() + } + } + + private func cancelNativeTabTracking() { + tabBar.cancelControlTrackingRecursively() + tabBar.gestureRecognizers?.forEach { recognizer in + guard recognizer !== profileLongPressRecognizer else { return } + recognizer.isEnabled = false + recognizer.isEnabled = true + } + tabBar.isUserInteractionEnabled = false + DispatchQueue.main.async { [weak self] in + self?.tabBar.cancelControlTrackingRecursively() + self?.tabBar.isUserInteractionEnabled = true + } } private func applyNativeTabBarAppearance() { @@ -535,6 +648,7 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { UIColor(red: 0.96, green: 0.96, blue: 0.96, alpha: 1) let unselected = UIColor(red: 150 / 255, green: 156 / 255, blue: 163 / 255, alpha: 1) + updateNativeTabTitles() refreshProfileAvatarImageIfNeeded() updateNativeTabImages(accent: accent) @@ -566,6 +680,13 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate { } } + private func updateNativeTabTitles() { + tabBar.items?.forEach { item in + guard let tab = NativeTab(tag: item.tag) else { return } + item.title = tab.localizedTitle() + } + } + private func nativeTabImage(for tab: NativeTab, selected: Bool, accent: UIColor) -> UIImage { guard tab == .settings else { return tab.iconImage @@ -630,6 +751,22 @@ private extension UIColor { } } +private extension String { + var nonEmpty: String? { + isEmpty ? nil : self + } +} + +private extension UIView { + func cancelControlTrackingRecursively() { + if let control = self as? UIControl { + control.cancelTracking(with: nil) + control.isHighlighted = false + } + subviews.forEach { $0.cancelControlTrackingRecursively() } + } +} + struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { // Register MPV player bridge before Compose initializes diff --git a/iosApp/iosApp/Player/MPVPlayerBridge.swift b/iosApp/iosApp/Player/MPVPlayerBridge.swift index 41d492574..563a692e4 100644 --- a/iosApp/iosApp/Player/MPVPlayerBridge.swift +++ b/iosApp/iosApp/Player/MPVPlayerBridge.swift @@ -60,6 +60,7 @@ final class MPVPlayerBridgeImpl: NSObject, NuvioPlayerBridge { ) } func setPlaybackSpeed(speed: Float) { playerVC?.setSpeed(speed) } + func setMuted(muted: Bool) { playerVC?.setMuted(muted) } func setResizeMode(mode: Int32) { playerVC?.setResize(Int(mode)) } // Audio tracks @@ -517,6 +518,11 @@ final class MPVPlayerViewController: UIViewController { mpv_set_property(mpv, "speed", MPV_FORMAT_DOUBLE, &s) } + func setMuted(_ muted: Bool) { + guard mpv != nil else { return } + setFlag("mute", muted) + } + func setResize(_ mode: Int) { guard mpv != nil else { return } switch mode { From 18d004ffe0232e7c6028d2cd11039136e7dc59de Mon Sep 17 00:00:00 2001 From: WhiteGiso Date: Thu, 18 Jun 2026 14:59:22 +0200 Subject: [PATCH 5/9] Add native liquid glass profile tab handling --- .../app/core/ui/NativeTabBridge.android.kt | 2 - .../commonMain/kotlin/com/nuvio/app/App.kt | 10 +- .../com/nuvio/app/core/ui/NativeTabBridge.kt | 2 - .../features/profiles/ProfileSwitcherTab.kt | 15 +- .../nuvio/app/core/ui/NativeTabBridge.ios.kt | 8 - iosApp/iosApp/ContentView.swift | 188 ++++++++++-------- 6 files changed, 116 insertions(+), 109 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt index 900c3489b..8e860fa35 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.android.kt @@ -23,5 +23,3 @@ internal actual fun publishNativeProfileTabIcon( avatarImageUrl: String?, avatarBackgroundColorHex: String?, ) = Unit - -internal actual fun notifyNativeProfileSwitcherPopupDismissed() = Unit diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index 755ffd9cd..21b046131 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -103,6 +103,7 @@ import com.nuvio.app.core.ui.NativeTabBridge import com.nuvio.app.core.ui.isLiquidGlassNativeTabBarSupported import com.nuvio.app.core.ui.localizedContinueWatchingSubtitle import com.nuvio.app.core.ui.nuvio +import com.nuvio.app.core.ui.nuvioBottomNavigationBarInsets import com.nuvio.app.features.auth.AuthScreen import com.nuvio.app.features.addons.AddonRepository import com.nuvio.app.features.catalog.CatalogRepository @@ -1422,6 +1423,11 @@ private fun MainAppContent( val isTabletLayout = maxWidth >= 768.dp val useNativeBottomTabs = liquidGlassNativeTabBarSupported && liquidGlassNativeTabBarEnabled && initialHomeReady + val nativeTabSafeBottomPadding = nuvioBottomNavigationBarInsets() + .asPaddingValues() + .calculateBottomPadding() + val nativeProfileTabAnchorBottomPadding = + nativeTabSafeBottomPadding + NuvioTokens.Space.s10 val tabsRouteActive = currentBackStackEntry?.destination?.hasRoute() == true val onProfileSelected: (NuvioProfile) -> Unit = { profile -> nativeProfileSwitcherVisible = false @@ -1596,7 +1602,9 @@ private fun MainAppContent( nativeProfileSwitcherVisible = false onSwitchProfile() }, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(bottom = nativeProfileTabAnchorBottomPadding), ) } } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt index 18299c1fa..2b9d48160 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.kt @@ -102,5 +102,3 @@ internal expect fun publishNativeProfileTabIcon( avatarImageUrl: String?, avatarBackgroundColorHex: String?, ) - -internal expect fun notifyNativeProfileSwitcherPopupDismissed() diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt index ba179788b..5e9b47952 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/profiles/ProfileSwitcherTab.kt @@ -72,7 +72,6 @@ import coil3.compose.AsyncImage import com.nuvio.app.core.ui.NuvioTokens import com.nuvio.app.core.ui.nuvio import com.nuvio.app.isIos -import com.nuvio.app.core.ui.notifyNativeProfileSwitcherPopupDismissed import kotlinx.coroutines.delay import kotlinx.coroutines.launch import nuvio.composeapp.generated.resources.* @@ -352,6 +351,7 @@ fun NativeProfileSwitcherPopup( val activeProfile = profileState.activeProfile val profiles = profileState.profiles val avatars by AvatarRepository.avatars.collectAsStateWithLifecycle() + val tokens = MaterialTheme.nuvio val haptic = LocalHapticFeedback.current val density = LocalDensity.current @@ -415,7 +415,6 @@ fun NativeProfileSwitcherPopup( ) } } else if (popupVisible) { - notifyNativeProfileSwitcherPopupDismissed() launch { popupAlpha.animateTo(0f, tween(180, easing = FastOutSlowInEasing)) } launch { popupScale.animateTo(0.85f, tween(200, easing = FastOutSlowInEasing)) } launch { @@ -437,7 +436,7 @@ fun NativeProfileSwitcherPopup( if (popupVisible && profiles.isNotEmpty() && !isSwitchingProfile) { Popup( alignment = Alignment.BottomCenter, - offset = IntOffset(0, with(density) { -84.dp.roundToPx() }), + offset = IntOffset(0, with(density) { -NuvioTokens.Space.s64.roundToPx() }), properties = PopupProperties(focusable = true), onDismissRequest = onDismissRequest, ) { @@ -450,16 +449,16 @@ fun NativeProfileSwitcherPopup( scaleY = popupScale.value translationY = popupTranslateY.value } - .shadow(16.dp, RoundedCornerShape(28.dp)) + .shadow(tokens.elevation.overlay, tokens.shapes.sheet) .background( - MaterialTheme.colorScheme.surfaceContainerHigh, - RoundedCornerShape(28.dp), + tokens.colors.surfaceSheet, + tokens.shapes.sheet, ) - .padding(16.dp), + .padding(tokens.spacing.sheetPadding), ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(tokens.spacing.cardPadding), verticalAlignment = Alignment.Top, ) { profiles.forEachIndexed { index, profile -> diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt index 5fad29aab..6a3c7e7f6 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/core/ui/NativeTabBridge.ios.kt @@ -17,7 +17,6 @@ private const val nativeProfileNameKey = "NuvioNativeProfileName" private const val nativeProfileAvatarColorKey = "NuvioNativeProfileAvatarColor" private const val nativeProfileAvatarUrlKey = "NuvioNativeProfileAvatarURL" private const val nativeProfileAvatarBackgroundColorKey = "NuvioNativeProfileAvatarBackgroundColor" -private const val nativeProfileSwitcherPopupDismissedNotification = "NuvioNativeProfileSwitcherPopupDismissed" private const val nativeTabChromeDidChangeNotification = "NuvioNativeTabChromeDidChange" internal actual fun isLiquidGlassNativeTabBarSupported(): Boolean { @@ -69,13 +68,6 @@ internal actual fun publishNativeProfileTabIcon( notifyNativeTabChromeChanged() } -internal actual fun notifyNativeProfileSwitcherPopupDismissed() { - NSNotificationCenter.defaultCenter.postNotificationName( - nativeProfileSwitcherPopupDismissedNotification, - null, - ) -} - private fun publishBool(key: String, value: Boolean) { NSUserDefaults.standardUserDefaults.setBool(value, forKey = key) notifyNativeTabChromeChanged() diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index ac5db8145..c8fb0338b 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -253,7 +253,7 @@ private enum NuvioNativeTabIcon { } } -final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGestureRecognizerDelegate { +final class RootComposeViewController: UIViewController, UITabBarDelegate { private enum NativeTab: String, CaseIterable { case home = "Home" case search = "Search" @@ -314,18 +314,17 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes private static let nativeProfileAvatarColorKey = "NuvioNativeProfileAvatarColor" private static let nativeProfileAvatarURLKey = "NuvioNativeProfileAvatarURL" private static let nativeProfileAvatarBackgroundColorKey = "NuvioNativeProfileAvatarBackgroundColor" - private static let nativeProfileSwitcherPopupDismissedNotification = Notification.Name("NuvioNativeProfileSwitcherPopupDismissed") private static let nativeTabChromeDidChangeNotification = Notification.Name("NuvioNativeTabChromeDidChange") private let contentController: UIViewController private let tabBar = UITabBar() + private let profileTabTouchOverlay = UIControl() private var contentBottomToViewBottom: NSLayoutConstraint? private var tabBarHeightConstraint: NSLayoutConstraint? private var userDefaultsObserver: NSObjectProtocol? private var tabChromeObserver: NSObjectProtocol? - private var profileSwitcherPopupObserver: NSObjectProtocol? - private var profileLongPressRecognizer: UILongPressGestureRecognizer? - private var suppressNextProfileSelection = false + private var profileTouchRestoreTab: NativeTab? + private var profileLongPressHandled = false private var profileAvatarImageURL: String? private var profileAvatarImageTask: URLSessionDataTask? private var profileAvatarImage: UIImage? @@ -372,9 +371,6 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes if let tabChromeObserver { NotificationCenter.default.removeObserver(tabChromeObserver) } - if let profileSwitcherPopupObserver { - NotificationCenter.default.removeObserver(profileSwitcherPopupObserver) - } profileAvatarImageTask?.cancel() } @@ -383,30 +379,16 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes updateTabBarHeight() } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + updateProfileTabTouchOverlayFrame() + } + func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let tab = NativeTab(tag: item.tag) else { return } - if tab == .settings && suppressNextProfileSelection { - suppressNextProfileSelection = false - restoreNativeTabFocus(to: currentNativeSelectedTab) - return - } selectNativeTab(tab) } - func gestureRecognizer( - _ gestureRecognizer: UIGestureRecognizer, - shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer - ) -> Bool { - gestureRecognizer === profileLongPressRecognizer - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { - if gestureRecognizer === profileLongPressRecognizer { - return nativeTab(at: touch.location(in: tabBar)) == .settings - } - return true - } - override var childForHomeIndicatorAutoHidden: UIViewController? { immersiveController(in: contentController) ?? contentController } @@ -486,18 +468,13 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes item.tag = tab.tag return item } - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleNativeProfileTabLongPress(_:))) - longPressRecognizer.delegate = self - longPressRecognizer.minimumPressDuration = 0.45 - longPressRecognizer.cancelsTouchesInView = true - tabBar.addGestureRecognizer(longPressRecognizer) - profileLongPressRecognizer = longPressRecognizer tabBar.selectedItem = tabBar.items?.first applyNativeTabBarAppearance() tabBar.alpha = 0 tabBar.isHidden = true view.addSubview(tabBar) + configureProfileTabTouchOverlay() let heightConstraint = tabBar.heightAnchor.constraint(equalToConstant: tabBarHeight) tabBarHeightConstraint = heightConstraint NSLayoutConstraint.activate([ @@ -524,14 +501,6 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes ) { [weak self] _ in self?.syncNativeTabChrome(animated: true) } - - profileSwitcherPopupObserver = NotificationCenter.default.addObserver( - forName: Self.nativeProfileSwitcherPopupDismissedNotification, - object: nil, - queue: .main - ) { [weak self] _ in - self?.suppressNextProfileSelection = false - } } private var tabBarHeight: CGFloat { @@ -540,6 +509,7 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes private func updateTabBarHeight() { tabBarHeightConstraint?.constant = tabBarHeight + updateProfileTabTouchOverlayFrame() } private func syncNativeTabChrome(animated: Bool) { @@ -551,15 +521,18 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes contentBottomToViewBottom?.isActive = true if visible { tabBar.isHidden = false + profileTabTouchOverlay.isHidden = false } let changes = { self.tabBar.alpha = visible ? 1 : 0 + self.profileTabTouchOverlay.alpha = visible ? 1 : 0 self.view.layoutIfNeeded() } let completion: (Bool) -> Void = { _ in self.tabBar.isHidden = !visible + self.profileTabTouchOverlay.isHidden = !visible } if animated && view.window != nil { @@ -583,13 +556,33 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes @objc private func handleNativeProfileTabLongPress(_ recognizer: UILongPressGestureRecognizer) { guard recognizer.state == .began else { return } - suppressNextProfileSelection = true - let tabToRestore = currentNativeSelectedTab - cancelNativeTabTracking() - DispatchQueue.main.async { [weak self] in - self?.restoreNativeTabFocus(to: tabToRestore) + profileLongPressHandled = true + DispatchQueue.main.async { NativeTabBridgeKt.nativeProfileTabLongPress() } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in + self?.restoreProfileTabTouchIfNeeded() + } + } + + @objc private func handleNativeProfileTabTouchDown() { + profileTouchRestoreTab = currentNativeSelectedTab + profileLongPressHandled = false + } + + @objc private func handleNativeProfileTabTap() { + if profileLongPressHandled { + profileLongPressHandled = false + restoreProfileTabTouchIfNeeded() + return + } + profileTouchRestoreTab = nil + selectNativeTab(.settings) + } + + @objc private func handleNativeProfileTabTouchCancel() { + profileLongPressHandled = false + restoreProfileTabTouchIfNeeded() } private var currentNativeSelectedTab: NativeTab { @@ -597,50 +590,78 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes return NativeTab(rawValue: rawValue) ?? .home } - private func nativeTab(at point: CGPoint) -> NativeTab? { - guard tabBar.bounds.contains(point), tabBar.bounds.width > 0 else { return nil } - let tabs = NativeTab.allCases - let rawIndex = Int((point.x / tabBar.bounds.width) * CGFloat(tabs.count)) - let clampedIndex = min(max(rawIndex, 0), tabs.count - 1) - return tabs[clampedIndex] - } - private func selectNativeTab(_ tab: NativeTab) { tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == tab.tag }) UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey) NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue) } - private func restoreNativeTabFocus(to tab: NativeTab) { - guard let item = tabBar.items?.first(where: { $0.tag == tab.tag }) else { return } - UserDefaults.standard.set(tab.rawValue, forKey: Self.nativeSelectedTabKey) - NativeTabBridgeKt.nativeTabSelect(tabName: tab.rawValue) + private func configureProfileTabTouchOverlay() { + profileTabTouchOverlay.backgroundColor = .clear + profileTabTouchOverlay.isOpaque = false + profileTabTouchOverlay.isExclusiveTouch = true + profileTabTouchOverlay.accessibilityLabel = NativeTab.settings.localizedTitle() + profileTabTouchOverlay.accessibilityTraits = .button + profileTabTouchOverlay.addTarget( + self, + action: #selector(handleNativeProfileTabTouchDown), + for: .touchDown + ) + profileTabTouchOverlay.addTarget( + self, + action: #selector(handleNativeProfileTabTap), + for: .touchUpInside + ) + profileTabTouchOverlay.addTarget( + self, + action: #selector(handleNativeProfileTabTouchCancel), + for: [.touchCancel, .touchUpOutside] + ) - tabBar.cancelControlTrackingRecursively() + let longPressRecognizer = UILongPressGestureRecognizer( + target: self, + action: #selector(handleNativeProfileTabLongPress(_:)) + ) + longPressRecognizer.minimumPressDuration = 0.45 + longPressRecognizer.cancelsTouchesInView = true + profileTabTouchOverlay.addGestureRecognizer(longPressRecognizer) - let restoreDuration: TimeInterval = 0.24 - UIView.animate( - withDuration: restoreDuration, - delay: 0, - options: [.beginFromCurrentState, .allowUserInteraction, .curveEaseInOut] - ) { - self.tabBar.selectedItem = item - self.tabBar.layoutIfNeeded() - } + profileTabTouchOverlay.alpha = 0 + profileTabTouchOverlay.isHidden = true + view.addSubview(profileTabTouchOverlay) + updateProfileTabTouchOverlayFrame() } - private func cancelNativeTabTracking() { - tabBar.cancelControlTrackingRecursively() - tabBar.gestureRecognizers?.forEach { recognizer in - guard recognizer !== profileLongPressRecognizer else { return } - recognizer.isEnabled = false - recognizer.isEnabled = true + private func restoreProfileTabTouchIfNeeded() { + let tab = profileTouchRestoreTab ?? currentNativeSelectedTab + tabBar.selectedItem = tabBar.items?.first(where: { $0.tag == tab.tag }) + profileTouchRestoreTab = nil + } + + private func updateProfileTabTouchOverlayFrame() { + let tabCount = CGFloat(NativeTab.allCases.count) + guard tabCount > 0, tabBar.bounds.width > 0 else { + profileTabTouchOverlay.frame = .zero + return } - tabBar.isUserInteractionEnabled = false - DispatchQueue.main.async { [weak self] in - self?.tabBar.cancelControlTrackingRecursively() - self?.tabBar.isUserInteractionEnabled = true + + let itemWidth = tabBar.bounds.width / tabCount + let settingsIndex = CGFloat(NativeTab.settings.tag) + let visualIndex: CGFloat + if tabBar.effectiveUserInterfaceLayoutDirection == .rightToLeft { + visualIndex = tabCount - 1 - settingsIndex + } else { + visualIndex = settingsIndex } + let overlayFrameInTabBar = CGRect( + x: itemWidth * visualIndex, + y: 0, + width: itemWidth, + height: tabBar.bounds.height + ) + profileTabTouchOverlay.frame = tabBar.convert(overlayFrameInTabBar, to: view) + profileTabTouchOverlay.alpha = tabBar.alpha + view.bringSubviewToFront(profileTabTouchOverlay) } private func applyNativeTabBarAppearance() { @@ -685,6 +706,7 @@ final class RootComposeViewController: UIViewController, UITabBarDelegate, UIGes guard let tab = NativeTab(tag: item.tag) else { return } item.title = tab.localizedTitle() } + profileTabTouchOverlay.accessibilityLabel = NativeTab.settings.localizedTitle() } private func nativeTabImage(for tab: NativeTab, selected: Bool, accent: UIColor) -> UIImage { @@ -757,16 +779,6 @@ private extension String { } } -private extension UIView { - func cancelControlTrackingRecursively() { - if let control = self as? UIControl { - control.cancelTracking(with: nil) - control.isHighlighted = false - } - subviews.forEach { $0.cancelControlTrackingRecursively() } - } -} - struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { // Register MPV player bridge before Compose initializes From cb8072b470bbfc36aed489ccc04136396be35d9d Mon Sep 17 00:00:00 2001 From: Joe00011 <152079313+Joe00011@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:44:55 +0900 Subject: [PATCH 6/9] Add Japanese locale to locale_config.xml --- composeApp/src/androidMain/res/xml/locale_config.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/composeApp/src/androidMain/res/xml/locale_config.xml b/composeApp/src/androidMain/res/xml/locale_config.xml index 530bb0477..a0cb2d584 100644 --- a/composeApp/src/androidMain/res/xml/locale_config.xml +++ b/composeApp/src/androidMain/res/xml/locale_config.xml @@ -12,4 +12,5 @@ + From f5b32acd424525c2842a47f0fc49b330dfa63bec Mon Sep 17 00:00:00 2001 From: Joe00011 <152079313+Joe00011@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:51:28 +0900 Subject: [PATCH 7/9] Add Japanese language support to AppLanguage enum --- .../kotlin/com/nuvio/app/features/settings/AppLanguage.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt index 85bcf497a..5d54c88af 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/settings/AppLanguage.kt @@ -13,6 +13,7 @@ import nuvio.composeapp.generated.resources.lang_portuguese_portugal import nuvio.composeapp.generated.resources.lang_spanish import nuvio.composeapp.generated.resources.lang_turkish import nuvio.composeapp.generated.resources.lang_norwegian +import nuvio.composeapp.generated.resources.lang_japanese import nuvio.composeapp.generated.resources.settings_appearance_app_language_device import org.jetbrains.compose.resources.StringResource @@ -33,6 +34,7 @@ enum class AppLanguage( SPANISH("es", Res.string.lang_spanish), TURKISH("tr", Res.string.lang_turkish), NORWEGIAN("nb", Res.string.lang_norwegian), + JAPANESE("ja", Res.string.lang_japanese), ; companion object { From 029abadbe660c7fd73c605353c5d2a4db3fd5b36 Mon Sep 17 00:00:00 2001 From: Joe00011 <152079313+Joe00011@users.noreply.github.com> Date: Sun, 21 Jun 2026 20:30:48 +0900 Subject: [PATCH 8/9] Add Japanese strings.xml translation --- .../composeResources/values-ja/strings.xml | 1870 +++++++++++++++++ 1 file changed, 1870 insertions(+) create mode 100644 composeApp/src/commonMain/composeResources/values-ja/strings.xml diff --git a/composeApp/src/commonMain/composeResources/values-ja/strings.xml b/composeApp/src/commonMain/composeResources/values-ja/strings.xml new file mode 100644 index 000000000..6bb7a42fd --- /dev/null +++ b/composeApp/src/commonMain/composeResources/values-ja/strings.xml @@ -0,0 +1,1870 @@ + + データソース、謝辞、ライセンス情報 + サポーター・コントリビューターのクレジット + 戻る + キャンセル + 閉じる + 削除 + 完了 + 編集 + インポート + 次へ + OK + 再生 + 前へ + 削除 + 並べ替え + デフォルトに戻す + 再開 + 再試行 + 保存 + 保存中… + 検証 + インストール中 + アドオン + 有効 + %1$d カタログ + 設定可能 + 無効 + 更新中 + %1$d リソース + 利用不可 + アドオンを設定 + アドオンを削除 + マニフェストURLを追加して、カタログ・メタデータ・ストリーム・字幕をNuvioに読み込みましょう。 + アドオンがありません + アドオンのURLを入力してください。 + アドオンURL + アドオンをインストール + マニフェストの詳細を読み込み中... + マニフェストURLを検証し、インストール前にアドオンの詳細を読み込んでいます。 + アドオンを確認中 + インストール失敗 + %1$s を検証し、正常に追加しました。 + インストール完了 + アドオンを下へ移動 + アドオンを上へ移動 + 有効 + アドオン + カタログ + アドオンを更新 + アドオンを追加 + インストール済みアドオン + 概要 + %1$d ID ルール + バージョン %1$s + 選択済み + JSONをコピー + %1$d コレクション、%2$d フォルダー + 「%1$s」を削除しますか?この操作は取り消せません。 + コレクションを削除 + カタログを追加 + フォルダーを追加 + すべてのジャンル + インストール済みアドオンからカタログを追加して、このフォルダーの表示内容を設定してください。 + カタログソースがありません + 選択 + 絵文字 + 画像URL + なし + カバー + コレクションを作成 + 完了 + コレクションを編集 + フォルダーを編集 + コレクションエディターと同じ構造で、フォルダーの識別情報・表示・カタログソースを設定してください。 + 追加して始めましょう。 + フォルダーがありません + フォルダー + ジャンルフィルター + カバー画像のみ表示します + タイトルを非表示 + 新しいフォルダー + このコレクションをホームのすべてのカタログより上に表示します。複数のピン留めコレクションはコレクション作成順に並びます。 + カタログより上にピン留め + バックドロップ画像URL(任意) + フォルダー名 + アニメーションGIF URL(フォーカス中のみ再生) + コレクション名 + 変更を保存 + 保存 + 外観 + 基本情報 + カタログソース + このフォルダーに集約するアドオンカタログを選択してください。 + カタログを選択 + ジャンルを選択 + %1$d 件選択中 + %1$d カタログ + %1$d 件選択中 + ポスター + 正方形 + ワイド + すべてのカタログを1つのタブにまとめる + 「すべて」タブを表示 + 設定されている場合、静止カバーの代わりにGIFを再生します。 + 設定時にGIFを表示 + %1$d ソース · %2$s + タイルの形状 + + タブ + 表示モード + TMDBソース + 公開リスト + 制作会社 + ネットワーク + コレクション + 人物 + 監督 + カスタム + 既製のソースを選択してください。追加後に編集・削除できます。 + TMDBの公開リストURLまたはURLの番号のみを貼り付けてください。 + スタジオ名で検索するか、TMDBの会社ID/URLを貼り付けて直接追加してください。 + ネットワークIDを入力してください。主要なネットワークはプリセットとクイックフィルターで利用できます。 + 映画コレクション名を検索するか、TMDBのコレクションIDを貼り付けてください。 + TMDBの人物IDまたはURLを入力して、出演作品の行を作成します。 + TMDBの人物IDまたはURLを入力して、監督作品の行を作成します。 + 任意のフィルターを使ってTMDBのライブ行を作成します。不要なフィルターは空白のままにしてください。 + TMDBの公開リスト + ネットワークID + コレクションID + 人物ID + 制作会社名、ID、またはURL + TMDBのIDまたはURL + https://www.themoviedb.org/list/8504994 または 8504994 + Netflix: 213、HBO: 49、Disney+: 2739 + スター・ウォーズ コレクション: 10 + Marvel Studios、420、または会社URL + Tom Hanks: 31、または人物URL + 例:Marvel Studios、420、または https://www.themoviedb.org/company/420 + 例:スター・ウォーズ コレクション、ハリー・ポッター コレクション、またはコレクションURL + ID例:Netflix 213、HBO 49、Disney+ 2739 + 例:https://www.themoviedb.org/list/8504994 または 8504994 + 例:https://www.themoviedb.org/person/31-tom-hanks または 31 + 表示タイトル + 行/タブ名として表示されます。空白の場合はNuvioがソースから自動生成します。 + マーベル映画、Netflixオリジナル、Pixar + Tom Hanks 映画、お気に入り俳優 + Christopher Nolan 映画、お気に入り監督 + アクション映画、韓国ドラマ、2024年アニメ + 検索結果 + TMDBコレクション + TMDB 制作会社 %1$d + TMDBコレクション %1$d + タイプ + 映画 + シリーズ + 両方 + 並び順 + フィルター + 不要なフィルターは空白のままにしてください。 + クイックジャンル + クイック言語 + クイック国 + クイックキーワード + クイックスタジオ + クイックネットワーク + ジャンルID + TMDBのジャンル番号を使用してください。AND条件はカンマ、OR条件はパイプで区切ってください。 + 28,12 + 18,35 + 公開・放送開始日 + 公開・放送終了日 + YYYY-MM-DD形式で入力してください(例:2024-01-01)。 + 2020-01-01 + 2024-12-31 + 最低評価 + 最高評価 + TMDBの評価(0〜10)。例:7.0 + 7.0 + 10 + 最低投票数 + 投票数の少ないマイナー作品を除外できます。例:100 + 100 + 原語 + 2文字の言語コードを使用してください(例:en、ko、ja、hi)。 + en, ko, ja, hi + 制作国 + 2文字の国コードを使用してください(例:US、KR、JP、IN)。 + US, KR, JP, IN + キーワードID + TMDBのキーワード番号を使用してください。クイックチップで一般的な例を入力できます。 + スーパーヒーロー: 9715 + 会社ID + スタジオ/会社IDを使用してください。クイックチップで一般的な例を入力できます。 + Marvel Studios: 420 + ネットワークID + シリーズのみ。Netflix 213、HBO 49などのネットワークIDを使用してください。 + Netflix: 213 + + 4桁の年を入力してください(例:2024)。 + 2024 + プリセット + 検索 + ソースを追加 + Traktリストを追加 + Traktリストを編集 + Traktリスト + Traktリスト + タイトル、Trakt URL、またはリストIDで検索 + 公開TraktリストのURLまたは数値リストIDを使用するか、名前で検索してください。 + 週末に見る、受賞作品 + 検索結果 + トレンドリスト + 人気リスト + 並び順 + 昇順 + 降順 + リスト順 + 追加日時 + タイトル + 公開日 + 上映時間 + 人気 + 評価率 + 投票数 + Traktリスト名、URL、またはIDを入力 + TraktリストIDまたはURLを入力 + Traktリストを読み込めませんでした + Traktリストが見つかりません + 解決済みTraktリスト + Traktリスト %1$d + アクション + アドベンチャー + アニメ + コメディ + ホラー + SF + ドラマ + クライム + リアリティ + 英語 + 韓国語 + 日本語 + ヒンディー語 + スペイン語 + アメリカ + 韓国 + 日本 + インド + イギリス + スーパーヒーロー + 原作小説あり + タイムトラベル + 宇宙 + Marvel + Disney + Pixar + Lucasfilm + Warner Bros. + Netflix + HBO + Disney+ + Prime Video + Hulu + オリジナル + 人気 + 高評価 + 新着 + 投票数 + 視聴地域 + 視聴可能な国のISO 3166-1国コード。例:US、GB + クイック視聴地域 + 視聴プロバイダーID + TMDBの視聴プロバイダーIDを使用してください。AND条件はカンマ、OR条件はパイプで区切ってください。 + 8|337|350 + クイック視聴プロバイダー + Netflix + Prime Video + Disney+ + Apple TV+ + Hulu + TMDBリスト + TMDB映画コレクション + 制作会社 + ネットワーク + 人物 + 監督 + TMDBディスカバー + コレクションを作成してカタログを整理しましょう。 + コレクションがありません + %1$d フォルダー + アイテムが見つかりません + フォルダーが見つかりません + コレクション + コレクションをインポート + JSON + コレクションのJSONを下に貼り付けてください。 + インポート + 新しいコレクション + ピン留め + すべて + マイコレクション + ❤️ Tapframe とフレンズが制作 + バージョン %1$s (%2$s) + オフ + オン + 一時停止 + 再読み込み + すでにアカウントをお持ちですか? + アカウントなしで続ける + アカウントを作成 + アカウントをお持ちでないですか? + メールアドレス + または + パスワード + サインインしてライブラリと視聴進捗にアクセス + サインイン + サインアップしてデバイス間でデータを同期 + サインアップ + データはこのデバイスにのみ保存されます + いつでも、どこでも、すべてをストリーム + おかえりなさい + ライブラリ + Traktライブラリ + ホーム + ライブラリ + プロフィール + 検索 + 音声トラック + 音声 + 自動同期 + 太字 + 内蔵 + 下端オフセット + キャプチャ + プレーヤーを閉じる + + 再生中 + E%1$d + S%1$dE%2$d + S%1$dE%2$d • %3$s + エピソード + フォントサイズ + %1$dsp + 操作をロック + 字幕の行を読み込み中... + 字幕が見つかりません + 字幕を読み込めませんでした + 音声トラックがありません + エピソードがありません + ストリームが見つかりません + なし + 縁取り + 縁取りの色 + エピソード + ソース + ストリーム + 再生エラー + 再生中 + タップして字幕を取得 + 戻る + 再読み込み + リセット + デフォルトに戻す + 塗りつぶし + フィット + ズーム + 10秒戻る + -%1$ds + +%1$ds + -%1$ds + +%1$ds + 10秒進む + ソース + スタイル + アドオンの字幕を先に選択してください + 字幕 + 字幕ディレイ + 字幕 + テキストの不透明度 + 明るさ %1$s + 音量 %1$s + ミュート中 + ダウンロード済み + 放送 + 未定 + タップしてロック解除 + トラック %1$d + 操作のロックを解除 + 視聴中 + プロフィールを追加 + 検索をクリア + 発見 + インストール済みアドオンが有効な検索結果を返しませんでした。 + 検索に失敗しました + 検索する前に、アドオンを少なくとも1つインストールして有効にしてください。 + 有効なアドオンがありません + インストール済みの検索可能なカタログで、このクエリに一致するものが見つかりませんでした。 + 結果が見つかりません + インストール済みアドオンにカタログ検索機能がありません。 + 検索可能なカタログがありません + 映画・シリーズを検索... + 最近の検索 + 最近の検索から削除 + アプリについて + 一般 + アカウント + アドオン + 詳細設定 + レイアウト + コンテンツと探索 + 視聴を続ける + 連携サービス + ホームレイアウト + 連携 + ライセンスと帰属 + MDBList評価 + 詳細ページ + 通知 + 再生 + プラグイン + ポスターカードスタイル + 設定 + ストリーム + サポーターとコントリビューター + TMDB補完 + Trakt + アプリについて + アカウントと同期状況 + アカウント + 起動時とプロフィールの動作 + 詳細設定 + ホーム構成とポスタースタイル + 最新リリースをダウンロード + アップデートを確認 + アドオンと探索ソースを管理 + ダウンロードした映画とエピソードを管理 + ダウンロード + 一般 + 利用可能な連携を管理 + エピソード公開アラートの管理とテスト通知の送信 + ストリーム結果の表示とバッジURLルール + 別のプロフィールに切り替え + プロフィールを切り替え + Trakt連携画面を開く + 設定が見つかりません。 + 設定を検索... + 結果 + 起動時 + キャッシュ + 最後のプロフィールを記憶 + 起動時に最後に選択したプロフィールを記憶する + 「視聴を続ける」キャッシュをクリア + 「視聴を続ける」のキャッシュデータを削除して視聴進捗を更新する + キャッシュをクリアしました + アプリライセンス + データとサービス + 再生ライセンス + Nuvio Mobile + ソースコードとライセンス条件はプロジェクトリポジトリで確認できます。 + GNU General Public License v3.0のもとでライセンスされています。 + The Movie Database (TMDB) + NuvioはTMDB APIを使用して、映画・TV番組のメタデータ、アートワーク、予告編、キャスト、制作詳細、コレクション、おすすめ作品を取得しています。本製品はTMDB APIを使用していますが、TMDBの公式承認・認定を受けたものではありません。 + IMDb非商用データセット + NuvioはIMDbの非商用データセット(title.ratings.tsv.gzを含む)を使用してIMDb評価と投票数を取得しています。情報提供:IMDb(https://www.imdb.com)。許可を得て使用しています。IMDbデータは個人的・非商用目的でのみIMDの利用規約に基づいて使用できます。 + Trakt + Nuvioはアカウント認証、視聴履歴、進捗同期、ライブラリデータ、評価、リスト、コメントのためにTraktと連携しています。NuvioはTraktと提携・承認されたものではありません。 + Premiumize + Nuvioはアカウント認証、クラウドライブラリアクセス、キャッシュ確認、クラウド再生機能のためにPremiumizeと連携しています。NuvioはPremiumizeと提携・承認されたものではありません。 + TorBox + Nuvioはアカウント認証、クラウドライブラリアクセス、キャッシュ確認、クラウド再生機能のためにTorBoxと連携しています。NuvioはTorBoxと提携・承認されたものではありません。 + MDBList + NuvioはMDBListを使用して評価と外部スコアプロバイダーデータを取得しています。NuvioはMDBListと提携・承認されたものではありません。 + IntroDB + NuvioはIntroDB APIを使用して、スキップコントロールに使用するイントロ・振り返り・エンドクレジット・プレビューのタイムスタンプ(コミュニティ提供)を取得しています。NuvioはIntroDBと提携・承認されたものではありません。 + MPVKit + iOSビルドの再生に使用。 + MPVKit単体はLGPL v3.0でライセンスされています。libmpvおよびFFmpegライブラリを含むMPVKitバンドルも同様にLGPL v3.0でライセンスされています。 + AndroidX Media3 ExoPlayer 1.8.0 + Androidビルドの再生に使用。 + Apache License, Version 2.0のもとでライセンスされています。 + Traktリストを読み込み中… + Traktでこのタイトルを保存する場所を選択 + 寄付 + 詳細を見る + 削除 + 最初から再生 + 再生 + %1$d/10 + レビュー + ネタバレあり + Traktのレビューはまだありません。 + %1$d いいね + このコメントにはネタバレが含まれています。 + このコメントにはネタバレが含まれているため、非表示にしています。 + コメント + 予告編 + %1$s (%2$d) + 予告編 + 完了したエピソードがありません + ダウンロードがありません + %1$d 件のエピソードをダウンロード済み + ダウンロード中 + 映画 + シリーズ + ダウンロードを表示 + 完了 • %1$s + ダウンロード中 • %1$s + 失敗 + 一時停止中 • %1$s + 視聴済み + シーズン %1$d + スペシャル + 続きから再生 + ライブラリに追加 + 未視聴にする + 視聴済みにする + ライブラリから削除 + すべて見る + 手動で再生 + %1$s ロゴ + アカウント + アカウントを削除 + アカウントとすべての関連データが完全に削除されます。 + この操作は取り消せません。すべてのデータ、プロフィール、同期履歴が完全に削除されます。 + アカウントを削除しますか? + メールアドレス + サインインしていません + サインアウト + ログイン画面に戻ります。 + サインアウトしますか? + ステータス + 匿名 + サインイン済み + 同期バックエンド + AMOLED ブラック + OLEDスクリーン向けに純粋な黒背景を使用します。 + アプリの言語 + デバイスの言語 + 言語を選択 + 「視聴を続ける」セクションの設定 + Liquid Glass + iOS 26以降でネイティブのiPhoneタブバーを使用します。オン中はタブバーからのプロフィール即時切り替えが利用できません。 + カードの幅と角の丸みを調整します。 + ディスプレイ + ホーム + テーマ + コレクション • %1$s + 表示名 + ホーム画面の行を設定するには、ボード対応カタログを持つアドオンをインストールしてください。 + ホームカタログがありません + ヒーローソース + 非表示 + ホームにフォーカスを維持 + %1$s • 上限に達しました(最大 %2$d) + ヒーローソースが選択されていません + ヒーローに含まない + 移動するにはコレクションのトップへのピン留めを外してください + ピン留め + トップにピン留め + 並べ替え + カタログ + カタログとコレクション + コレクション + ホームレイアウト + ヒーローカタログ + %1$d / %2$d 件選択中 + ヒーローセクションを表示 + ホーム上部にヒーローカルーセルを表示します。 + 未公開コンテンツを非表示 + まだ公開されていない映画やシリーズを非表示にします。 + カタログの下線を非表示 + アプリ全体のカタログ・コレクションタイトル下のアクセントラインを非表示にします。 + %1$d / %2$d カタログ表示中 • %3$d ヒーローソース選択中 + カタログは名前の変更や並べ替えが必要なときのみ開いてください。 + 表示 + 値を隠す + プレーヤー、字幕、自動再生 + 角の丸み + ポスターカードスタイル + + カスタム + カードの幅と角の丸みを調整します。 + ラベルを非表示 + 横向きポスター + ライブプレビュー + %1$s (%2$s) + 角の丸み: %1$ddp + 高さ: %1$ddp + 幅: %1$ddp + クラシック + ピル + 丸み大 + シャープ + 丸み小 + バランス + コンフォート + コンパクト + 高密度 + + 標準 + 値を表示 + プレーヤーから離脱後にアプリを開くと、続きから再生するポップアップを表示します。 + 起動時に再開プロンプトを表示 + ネタバレを避けるため「視聴を続ける」の次のエピソードのサムネイルをぼかします。 + 「視聴を続ける」で未視聴をぼかす + 放送前の今後のエピソードも「視聴を続ける」に表示します。 + 未放送の次のエピソードを表示 + 並び順 + 並び順 + デフォルト + すべてのアイテムを新着順に並べる + ストリーミングスタイル + 公開済みを先頭に、今後のものを末尾に表示 + ポスターカードスタイル + 起動時 + 次のエピソードの動作 + 表示 + ホーム画面に「視聴を続ける」セクションを表示します。 + 「視聴を続ける」を表示 + カード + TV風の横長カード + ポスター + アートワーク優先のポスターカード + ワイド + 情報密度の高い横長カード + 最も遠くまで視聴したエピソードに基づいて次のエピソードを表示します。再視聴時に最近視聴したエピソードを使用するには無効にしてください。 + 最後まで視聴したエピソードから次へ + 利用可能な場合はエピソードのサムネイルを優先します。 + 「視聴を続ける」でエピソードサムネイルを優先 + ホーム + ソース + コンテンツソースのインストール・削除・更新・並べ替え + JavaScriptスクレイパーリポジトリのインストールと内部テスト + ホームレイアウト・コンテンツ表示・ポスターの動作を調整 + 詳細画面とエピソード画面の設定 + ホームに表示するフォルダー付きのカスタムカタロググループを作成 + 連携 + メタデータ補完の設定 + 外部評価プロバイダー + リンクとライブラリアクセス用のアカウントを連携 + 連携サービス + これらの連携は試験的なものであり、今後維持・変更・削除される可能性があります。 + クラウドライブラリ + 連携済みアカウントのファイルを参照して再生します。 + 再生可能なリンクを解決 + 結果に必要な場合、連携サービスに再生可能なリンクを要求します。このアイテムがそのサービスに追加される場合があります。 + 解決に使用するサービス + 再生可能なリンクを処理する連携アカウントを選択します。 + 先にアカウントを連携してください。 + アカウント + %1$s アカウントを連携します。 + ブラウザで %1$s アカウントをリンクします。 + %1$s APIキー + %1$s のAPIキーを入力してください。 + %1$s のAPIキーを入力 + 未設定 + 連携済み + %1$s を連携 + %1$s の連携を解除 + 連携を解除 + %1$s はこのデバイスで連携済みです。 + セキュアサインインを開始中... + リンクを開き、このコードを入力してNuvioを承認してください。 + コードをコピーしました。 + リンクを開く + 承認を待っています... + サインインを開始できませんでした。 + このサインイン方法はこのビルドでは設定されていません。 + このコードは期限切れです。再試行してください。 + リンクの準備 + リンクを事前準備 + 再生開始前に再生可能なリンクを解決します。 + 準備するリンク数 + できるだけ少ない数を使用してください。連携サービスは一定期間内に解決できるリンク数を制限する場合があります。映画やエピソードを開くと、「視聴する」を押さなくてもリンクが事前に準備されるため、その制限にカウントされる場合があります。 + 1リンク + %1$d リンク + フォーマット + 名前テンプレート + 結果名の表示形式を制御します。空白の場合は元の結果名を使用します。 + 説明テンプレート + 各結果に表示されるメタデータを制御します。空白の場合は元の結果詳細を使用します。 + フォーマットをリセット + 結果のフォーマットをデフォルトに戻します。 + Fusionスタイル + サイズバッジ + ストリーム結果とプレーヤーのソースパネルにファイルサイズバッジを表示します。 + アドオンロゴ + ストリームソースの横にアドオンのロゴと名前を表示します。 + 表示 + バッジの位置 + Fusionバッジとサイズバッジをストリームカードの上下どちらに表示するかを選択します。 + バッジの位置 + ストリームカード上のバッジの表示位置を選択してください。 + + + Fusionバッジ URL + Fusionスタイルのストリームバッジ JSON URLを最大 %1$d 件インポートできます。各URLは個別に更新・削除できます。 + インポートされたFusionスタイルのストリームバッジ JSON URLを管理します。 + バッジのインポートに失敗しました。 + バッジのJSON URLを入力してください。 + バッジURLはhttp://またはhttps://で始まる必要があります。 + バッジURLは最大 %1$d 件インポートできます。 + %1$d/%2$d URL、%3$d 件の有効なFusionバッジ + FusionバッジのURLがインポートされていません。 + Fusionバッジ JSON URL + %1$d/%2$d Fusion URLをインポート済み + 有効 + 無効 + %1$s、%2$d 件のバッジ有効、%3$d グループ + プレビュー + Fusionバッジプレビュー + このURLから %1$d 件のFusionスタイルバッジ + このURLにFusionスタイルのバッジ画像がありません。 + グループ %1$d + その他のFusionバッジ + APIキーを検証しました。 + このAPIキーを検証できませんでした。 + 評価を有効にする前に、下でMDBListのAPIキーを追加してください。 + MDBListから評価を取得するために必要です + APIキー + APIキー + MDBList評価を有効にする + メタデータ詳細画面で外部プロバイダーの評価を取得する + APIキー + 外部評価プロバイダー + MDBList評価 + アクション + 再生と保存のコントロール + キャスト + 主要キャスト一覧 + シネマティック背景 + ストリーム画面と同様に、コンテンツの後ろにぼかしたバックドロップを表示します。 + コレクション + 関連するコレクションやフランチャイズのレール + コメント + Traktのレビュー + 詳細 + 上映時間、ステータス、公開日、言語などの情報 + ヒーロー予告編再生 + 予告編が利用可能な場合、メタデータのヒーロー部分で予告編プレビューを再生します。 + エピソードカード + メタデータ画面でのエピソードの表示方法を選択します。 + 横型 + バックドロップスタイルの行カード + リスト + 詳細情報優先の縦積みカード + エピソード + シリーズのシーズンとエピソード一覧 + 未視聴エピソードをぼかす + ネタバレを避けるため、視聴済みになるまでエピソードのサムネイルをぼかします。 + グループ %1$d + この作品と似た作品 + 詳細ページのTMDBおすすめバックドロップ + なし + 概要 + あらすじ、評価、ジャンル、主要クレジット + 制作 + スタジオとネットワーク + 外観 + セクション + タブグループ %1$d + タブレイアウト + TVアプリのようにセクションをタブにグループ化します。タブグループごとに最大3つのセクションを割り当てられます。 + 予告編 + 予告編レールと再生ショートカット + Nuvioで通知が無効になっています。 + エピソード公開アラート + 保存済みシリーズの新しいエピソードが公開されたときにローカル通知をスケジュールします。 + Nuvioのシステム通知が無効になっています。アラートとテスト通知を受け取るには有効にしてください。 + このデバイスで現在 %1$d 件の公開アラートがスケジュールされています。 + アラート + テスト + テスト通知を送信 + テスト通知を送信中... + %1$s のローカルテスト通知を送信します。 + テスト通知を送るには、シリーズをライブラリに保存してください。 + テスト通知 + コミュニティ + Mobile・TV・Webを通じてNuvioを構築・支援している人々をご覧ください。 + 今月のサーバー・メンテナンス費用 + 達成済み。追加のサポートは開発費に充てられます。 + 100%%を超えた追加サポートは開発費に充てられます。 + 資金調達状況を読み込み中... + サポーターAPIが設定されていません。local.propertiesにDONATIONS_BASE_URLを追加してください。 + コントリビューター + サポーター + GitHubを開く + GitHubプロフィールは利用できません + メッセージはありません。 + コントリビューターを読み込み中... + サポーターを読み込み中... + コントリビューターを読み込めませんでした + サポーターを読み込めませんでした + コントリビューターが見つかりません。 + サポーターが見つかりません。 + コントリビューターを読み込めません。 + サポーターを読み込めません。 + 現在コントリビューターを読み込めません。 + 現在サポーターを読み込めません。 + 合計 %1$d コミット + 1月 + 2月 + 3月 + 4月 + 5月 + 6月 + 7月 + 8月 + 9月 + 10月 + 11月 + 12月 + %3$s年%1$s%2$s日 + インストール済みのすべてのアドオン + 有効なすべてのプラグイン + 許可するアドオン + 許可するプラグイン + Anime Skip + AnimeSkip クライアントID + AnimeSkip APIのクライアントIDを入力してください。anime-skip.comで取得できます。 + イントロ投稿を有効にする + イントロ・アウトロのタイムスタンプをコミュニティデータベースに投稿するボタンを表示します。 + IntroDB APIキー + タイムスタンプを投稿するためのIntroDB APIキーを入力してください。投稿に必要です。 + スキップタイムスタンプをAnimeSkipでも検索します(クライアントIDが必要)。 + 次のエピソードを自動再生 + プロンプトが表示されたら自動的に次のエピソードを開始します。 + デバイスデコーダーのみ + アプリデコーダーを優先(FFmpeg) + デバイスデコーダーを優先 + デコーダーの優先順位 + 外をタップして閉じる + 外をタップして保存して閉じる + %1$d 日 + %1$d 日 + %1$d 時間 + %1$d 時間 + ASS/SSA字幕にlibassを使用 + 試験的:高度なASS/SSAレンダリング(スタイル、位置、アニメーション) + 外部プレーヤー + 外部プレーヤーアプリ + 新しい再生をAndroidのデフォルト動画アプリまたはシステムの選択画面で開きます。 + 新しい再生を選択したインストール済みプレーヤーで開きます。 + 外部プレーヤーに字幕を転送 + 優先言語のアドオン字幕を取得して外部プレーヤーに渡します。 + 対応する外部プレーヤーがインストールされていません + プレーヤー + 内蔵 + 外部 + 新しい再生に使用するプレーヤーを選択します。 + 長押し速度 + 長押しで速度変更 + プレーヤー画面のどこかを長押しすると、一時的に再生速度を上げます。 + タッチジェスチャー + プレーヤー画面でのスワイプやダブルタップによるシーク・明るさ・音量調整を許可します。 + 無効な正規表現パターン + 最終リンクキャッシュ期間 + DV7 - HEVCフォールバック + DolbyVisionハードウェアをサポートしていないデバイス向けに、Dolby Vision Profile 7を標準HEVCにマップします + しきい値(分) + アウトロのタイムスタンプがない場合のフォールバック。 + %1$s 分 + 利用可能なアイテムがありません + 未設定 + デフォルト(メディアファイル) + デバイスの言語 + 原語 + TMDB補完が有効な必要があります + 強制 + なし + すべての字幕 + 動画のすべてのアドオン字幕を取得して表示します。 + 高速起動 + プレーヤーでリクエストするまでアドオン字幕の自動取得をスキップします。 + アドオン字幕の起動設定 + 優先言語のみ + アドオン字幕を取得しますが、優先言語の一致のみ表示します。 + 一気見グループを優先(次のエピソード) + 通常の自動再生ルールより先に同じソースプロファイル(同じアドオン/品質グループ)を試みます。 + 一気見グループを再利用 + 最後の一気見グループをセッション間で記憶して再利用します(「視聴を続ける」、詳細など)。 + 優先音声言語 + 優先言語 + プリセット + ストリーム名/タイトル/説明/アドオン/URLに対してマッチします。例:4K|2160p|Remux + 正規表現パターン + パターン未設定。例:4K|2160p|Remux + 1080p以上 + AVC / x264 + BluRay品質 + Dolby Atmos / DTS + 英語 + HDR / Dolby Vision + HEVC / x265 + CAM/TSを除外 + REMUX/HDRを除外 + 1080p 標準 + 4K / Remux + 720p 以下 + WEBソース + libassレンダリングモード + 標準キュー + エフェクトキャンバス + エフェクトOpenGL + オーバーレイキャンバス + オーバーレイOpenGL(推奨) + 最後のリンクを再利用 + キャッシュが有効な場合、同じ映画/エピソードの最後に機能したストリームを自動再生します + 第2音声言語 + 第2優先言語 + デコーダー + 次のエピソード + プレーヤー + スキップセグメント + ストリーム自動再生 + ストリーム選択 + 字幕と音声 + 字幕レンダリング + %1$d 件選択中 + 読み込みオーバーレイ + 最初の映像フレームが表示されるまで読み込み画面を表示します。 + 背景色 + 太字 + 字幕を太いフォントウェイトで表示します。 + 透明 + 縁取り + 縁取りの色 + 字幕テキストに縁取りを描画します。 + 優先言語の字幕のみ表示 + 優先する字幕言語に一致する字幕のみ表示します。 + 字幕サイズ + テキストの色 + 強制字幕を使用 + 字幕言語の設定に一致する場合、強制字幕を優先します。 + 垂直オフセット + イントロをスキップ + introdb.appを使用してイントロと振り返りを検出します。 + 自動再生のソース範囲 + インストール済みのすべてのアドオン + 自動再生はインストール済みアドオンのストリームのみを対象とします。 + すべてのソース + 自動再生はインストール済みアドオンと有効なプラグインの両方を使用できます。 + 有効なプラグインのみ + 自動再生は有効なプラグインのストリームのみを対象とします。 + インストール済みアドオンのみ + 自動再生はインストール済みアドオンのストリームのみを対象とします。 + ストリームの自動選択 + 最初のソースを自動再生 + 利用可能な最初のソースを自動的に再生します。 + 手動(ストリームを選択) + 常にソース一覧を表示して選択します。 + 正規表現に一致するソースを自動再生 + 正規表現パターンに一致するテキストを持つ最初のソースを再生します。 + ストリーム選択タイムアウト + 選択前にアドオンを待機する時間。 + しきい値(分) + 次のエピソードのしきい値モード + 終了前の分数 + パーセンテージ + しきい値(%) + アウトロのタイムスタンプがない場合のフォールバック。 + %1$s% + 即時 + %1$s 秒 + 無制限 + トンネル再生 + ハードウェアレベルの音声/映像同期。一部のAndroid TVデバイスで再生が改善される場合があります + 補完を有効にする前に、下で自分のTMDB APIキーを追加してください。 + APIキー + TMDB補完を有効にする + アドオンデータを強化するためのメタデータソースとしてTMDBを使用する + TMDBのv3 APIキーを入力してください。 + 言語コード + アートワーク + TMDBのロゴとバックドロップ画像 + 基本情報 + TMDBの説明、ジャンル、評価 + コレクション + 公開順のTMDB映画コレクション + クレジット + TMDBの写真付きキャスト、監督、脚本家 + 詳細 + TMDBの上映時間、ステータス、国、言語 + エピソード + TMDBのエピソードタイトル、概要、サムネイル、上映時間 + この作品と似た作品 + 詳細ページのTMDBおすすめバックドロップ + ネットワーク + TMDBのロゴ付きネットワーク + 制作会社 + TMDBの制作会社 + シーズンポスター + シリーズのメタデータ画面のシーズンセレクターにTMDBのシーズンポスターを使用します。 + 予告編 + 詳細の予告編セクション向けのTMDB動画から予告編候補を取得 + 個人APIキー + 言語 + タイトル、ロゴ、有効なフィールドのTMDBメタデータ言語 + 認証情報 + ローカライズ + モジュール + TMDB補完 + 承認後、自動的にリダイレクトされます。 + 認証 + コメント + メタデータページにTraktのレビューを表示する + Traktに連携 + %1$s として連携中 + Traktユーザー + 連携を解除 + ブラウザを開けませんでした + 機能 + ブラウザでTraktのサインインを完了してください + ウォッチリスト、視聴進捗、「視聴を続ける」、スクロブル、個人リストをTraktと同期します。 + local.propertiesにTraktの認証情報がありません(TRAKT_CLIENT_ID / TRAKT_CLIENT_SECRET)。 + Traktログインを開く + 保存アクションでTraktのウォッチリストと個人リストをターゲットにできます。 + Traktにサインインして、リストベースの保存とTraktライブラリモードを有効にしてください。 + ライブラリソース + 保存とコレクションの表示に使用するライブラリを選択 + ライブラリソース + ライブラリアイテムの保存・管理場所を選択してください + Trakt + Nuvioライブラリ + Traktライブラリを選択しました + Nuvioライブラリを選択しました + 視聴進捗 + 再開と「視聴を続ける」に使用する進捗ソースを選択 + 視聴進捗 + Traktスクロブルが有効な状態で、再開と「視聴を続ける」にTraktまたはNuvio同期のどちらを使用するかを選択してください。 + Trakt + Nuvio同期 + 視聴進捗ソースをTraktに設定しました + 視聴進捗ソースをNuvio同期に設定しました + 「似た作品」のソース + 詳細ページのおすすめ作品の取得元を選択 + 「似た作品」のソース + 詳細ページに表示するおすすめ作品のソースを選択してください。 + Trakt + TMDB + 「視聴を続ける」の期間 + 「視聴を続ける」に使用するTrakt履歴の範囲 + 「視聴を続ける」の期間 + 「視聴を続ける」に表示するTraktのアクティビティ範囲を選択してください。 + すべての履歴 + %1$d 日 + オーディエンススコア + IMDb + Letterboxd + Metacritic + Rotten Tomatoes + TMDB + Trakt + 不明 + アンバー + クリムゾン + エメラルド + オーシャン + ローズ + バイオレット + ホワイト + 次のエピソード + ソースを検索中… + %1$s で %2$d 秒後に再生… + 次のエピソードのサムネイル + 未放送 + スキップ + イントロをスキップ + アウトロをスキップ + 振り返りをスキップ + 字幕が見つかりません + アフリカーンス語 + アルバニア語 + アムハラ語 + アラビア語 + アルメニア語 + アゼルバイジャン語 + バスク語 + ベラルーシ語 + ベンガル語 + ボスニア語 + ブルガリア語 + ビルマ語 + カタルーニャ語 + 中国語 + 中国語(簡体字) + 中国語(繁体字) + クロアチア語 + チェコ語 + デンマーク語 + オランダ語 + 英語 + エストニア語 + フィリピン語 + フィンランド語 + フランス語 + ガリシア語 + ジョージア語 + ドイツ語 + ギリシャ語 + グジャラート語 + ヘブライ語 + ヒンディー語 + ハンガリー語 + アイスランド語 + インドネシア語 + アイルランド語 + イタリア語 + 日本語 + カンナダ語 + カザフ語 + クメール語 + 韓国語 + ラオス語 + ラトビア語 + リトアニア語 + マケドニア語 + マレー語 + マラヤーラム語 + マルタ語 + マラーティー語 + モンゴル語 + ネパール語 + ノルウェー語 + ペルシャ語 + ポーランド語 + ポルトガル語(ポルトガル) + ポルトガル語(ブラジル) + パンジャーブ語 + ルーマニア語 + ロシア語 + セルビア語 + シンハラ語 + スロバキア語 + スロベニア語 + スペイン語 + スペイン語(ラテンアメリカ) + スワヒリ語 + スウェーデン語 + タミル語 + テルグ語 + タイ語 + トルコ語 + ウクライナ語 + ウルドゥー語 + ウズベク語 + ベトナム語 + ウェールズ語 + ズールー語 + クリア + 続ける + 無視 + インストール + 後で + いいえ + 更新 + はい + アプリを終了しますか? + アプリを終了 + このカタログはアイテムを返しませんでした。 + タイトルが見つかりません + その他のアクション + Wi-Fiまたはモバイルデータ接続を確認して再試行してください。 + 監督 + 読み込みに失敗しました + この作品と似た作品 + Powered by TMDB + Powered by Trakt + シーズン + このアドオンはシリーズの動画を返しましたが、シーズンやエピソード番号が含まれていませんでした。 + このアドオンはこのシリーズのエピソードメタデータを提供しませんでした。 + このアドオンはまだエピソードを公開していません。 + デバイスはオンラインですが、Nuvioは必要なサーバーに到達できませんでした。 + 折りたたむ + もっと見る ▾ + 脚本 + すべてのジャンル + カタログ + %1$s • %2$s + 選択したカタログが発見アイテムを返せませんでした。 + 発見を読み込めませんでした + インストール済みアドオンに発見用のボード対応カタログがありません。 + 発見カタログがありません + 選択したカタログとフィルターではアイテムが見つかりませんでした。 + タイトルが見つかりません + 発見カタログを参照する前に、少なくとも1つのアドオンをインストールして有効にしてください。 + カタログを選択 + ジャンルを選択 + タイプを選択 + タイプ + 前のエピソードを未視聴にする + 前のエピソードを視聴済みにする + %1$s を未視聴にする + %1$s を視聴済みにする + 前のシーズンを視聴済みにする + 未視聴にする + 視聴済みにする + 次のエピソード + %1$s 視聴済み + 残り %1$d 時間 %2$d 分 + 残り %1$d 分 + ホームにカタログ行を読み込む前に、少なくとも1つのアドオンをインストールして有効にしてください。 + インストール済みアドオンは現在、必須エクストラなしでボード対応カタログを提供していません。 + ホームの行がありません + 詳細を見る + 再生と保存のコントロール + アクション + 主要キャスト一覧 + 関連するコレクションやフランチャイズのレール + コレクション + Traktコメントセクション + 上映時間、ステータス、公開日、言語などの情報 + 詳細 + シリーズのシーズンとエピソード一覧 + おすすめ作品レール + この作品と似た作品 + あらすじ、評価、ジャンル、主要クレジット + 概要 + スタジオとネットワーク + 制作 + 予告編レールと再生ショートカット + オンラインに戻りました + サーバーに接続できません + 空のレスポンスボディ + リクエストが HTTP %1$d で失敗しました + インターネット接続がありません + (%1$d 歳) + %1$s%2$s 生まれ + %1$s 没 + 代表作:%1$s + 最新作 + %1$s の詳細を読み込めませんでした + 人気作 + 問題が発生しました + 公開予定 + 削除 + キャンセル + PINを入力 + %1$s のPINを入力 + PINを忘れた場合 + PINが正しくありません + ロックされています。%1$d 秒後に再試行してください + カタログが読み込まれるとアバターの選択肢がここに表示されます。 + アバター:%1$s + 有効なhttp://またはhttps://の画像URLを入力してください。 + アバターを選択 + 下からアバターを選択してください。 + プロフィールを作成 + カスタムアバターURLを選択しました。 + カスタムアバターURL + 画像リンクを貼り付けるか、内蔵のアバターカタログを使用するには空白のままにしてください。 + https://example.com/avatar.png + 「%1$s」のすべてのデータが完全に削除されます。 + プロフィールを削除 + プロフィールを追加 + プロフィールを編集 + 現在のPINを入力 + 新しいPINを入力 + プロフィール %1$d + アバターを読み込み中... + プロフィールを管理 + プロフィール名 + 新しいプロフィール + プライマリアドオンオフ + プライマリアドオンオン + %1$s のPINロックを解除 + PINロックを解除 + 保存中... + セキュリティ + このプロフィールをロックするには、PINを設定してください。 + このプロフィールはPINで保護されています。 + このプロフィールのアバターを選択してください。 + PINロックを設定 + 名前のないプロフィール + プライマリアドオンを使用 + 個別のリストを管理する代わりに、メインプロフィールのアドオン設定を共有します。 + 誰が視聴しますか? + ダウンロード済み + 再開 + アクティブなスクレイパー + さらにアドオンを確認中… + ストリームリンクをコピー + ファイルをダウンロード + 外部プレーヤーで開く + 内蔵プレーヤーで開く + インストール済みのストリームアドオンが有効なストリームレスポンスを返せませんでした。 + ストリームを読み込めませんでした + このタイトルのストリームを読み込むには、先にアドオンをインストールしてください。 + インストール済みアドオンはこのタイプのタイトルのストリームを提供していません。 + ストリームアドオンがありません + インストール済みアドオンがこのタイトルのストリームを返しませんでした。 + S%1$d E%2$d + エピソード + S%1$dE%2$d - %3$s + 取得中… + ソースを検索中… + アドオンから字幕を読み込み中… + ストリームを検索中… + ストリームリンクをコピーしました + 直接のストリームリンクがありません + メタデータがありません + ストリームを更新 + %1$d%% から再開 + %1$s から再開 + サイズ %1$s + このストリームタイプはサポートされていません + 設定でアカウントを連携してください。 + TorBoxにキャッシュされていません。 + このリンクは期限切れです。結果を更新しています。 + このリンクを開けませんでした。 + 外部プレーヤーを開けませんでした + 先に設定で外部プレーヤーを選択してください + 外部プレーヤーが利用できません + 予告編を閉じる + 予告編を再生できません + Traktリストの読み込みに失敗しました + Traktリストの更新に失敗しました + %1$s • %2$s + アップデートの確認に失敗しました + ダウンロードに失敗しました + ダウンロードボディが空です + ダウンロードした更新ファイルが見つかりません。 + ダウンロード中 %1$d%% + インストールを開始できません + 最新バージョンを使用しています。 + Nuvioのアプリインストールを有効にして、戻って続けてください。 + アップデートをダウンロード中... + アップデートが見つかりません。 + 新しいバージョンをインストールする準備ができました。 + このビルドではアプリ内アップデートが利用できません。 + ダウンロードを準備中 + リリースノート + インストールを許可して続ける + アップデートが利用可能 + アップデートの状況 + そのアドオンはすでにインストールされています。 + 有効なアドオンURLを入力してください + マニフェストを読み込めませんでした + Nuvio + アカウントの削除に失敗しました + サインインに失敗しました + サインアウトに失敗しました + サインアップに失敗しました + カタログアイテムを読み込めませんでした。 + 次のエピソード + 次のエピソード • S%1$dE%2$d + %1$s ロゴ + コメントの読み込みに失敗しました + どのアドオンからも詳細を読み込めませんでした。 + ネットワーク + このコンテンツのメタデータを提供するアドオンがありません。 + ダウンロードに失敗しました + ダウンロードの進捗とコントロールをリアルタイムで表示します。 + ダウンロード + ダウンロード完了 + %1$s をダウンロード中 • %2$s + %1$s をダウンロード中 • %2$s / %3$s + ダウンロード失敗 + %1$s を一時停止中 + 削除 + %1$s を %2$s から削除しますか? + %1$s をライブラリから削除しますか? + ライブラリから削除しますか? + 映画 + 保存済みシリーズの新しいエピソードが公開されたときに通知します。 + エピソード公開アラートのプレビュー。 + テスト通知の送信に失敗しました。 + %1$s のテスト通知を送信しました。 + このストリームを再生できません。 + MPVプレーヤーエンジンが利用できません。アプリを再ビルドしてください。 + このプロフィールのPINが変更されました。このデバイスのロックを更新するには一度接続してください。 + PINロックを解除できませんでした。再試行してください。 + PINロックを解除するにはインターネットに接続してください。 + このPINはこのデバイスでオフライン確認できません。オンラインで一度解除してください。 + PINを設定できませんでした。再試行してください。 + PINを設定するにはインターネットに接続してください。 + このプロフィールはプライマリアドオンを使用しています。 + %1$s の読み込みに失敗しました + ストリーム + 埋め込み + 認証が拒否されました + ブラウザでTraktのサインインを完了してください + Traktに連携しました + Traktの連携を解除しました + 無効なTraktコールバック + 無効なTraktコールバック状態 + 無効なTraktトークンレスポンス + Traktライブラリの読み込みに失敗しました + リスト %1$d + Traktが認証コードを返しませんでした + Traktの認証情報がありません + Traktの進捗の読み込みに失敗しました + Traktの公開リスト + 有効なTraktリストIDまたはURLを入力してください + %1$d 件 + %1$d いいね + local.propertiesにTraktの認証情報がありません(TRAKT_CLIENT_ID)。 + TraktリストIDがありません + Traktリストに数値IDが含まれていませんでした + Traktリストが見つからないか、公開されていません + Traktのレート制限に達しました + Traktリクエストに失敗しました + Traktのサインイン完了に失敗しました + Traktユーザー + ウォッチリスト + Traktの認証が期限切れです + Traktリストが見つかりません + Traktリストの上限に達しました + Traktのレート制限に達しました + Traktリクエストに失敗しました + Traktウォッチリストへの追加に失敗しました + Traktリストへの追加に失敗しました + 互換性のあるTrakt IDがありません + 空のレスポンスボディ + TMDBソースを使用するには設定でTMDB APIキーを追加してください。 + TMDBコレクション %1$d + TMDBコレクションが見つかりません + TMDB制作会社 %1$d + TMDB会社が見つかりません + TMDB監督 %1$d + TMDBディスカバーがデータを返しませんでした + TMDBディスカバー + 有効なTMDB IDまたはURLを入力してください。 + TMDBリスト %1$d + TMDBリストが見つかりません + TMDBソースを読み込めませんでした + TMDBコレクションIDがありません + TMDBリストIDがありません + TMDB人物IDがありません + TMDBネットワーク %1$d + TMDBネットワークが見つかりません + TMDBの人物クレジットが見つかりません + TMDB人物 %1$d + TMDB人物が見つかりません + 予告編 + 不明 + アドオン + 保存済み + %1$s を再生 + %1$s を再開 + JSONが空です。 + コレクション %1$d のIDが空白です。 + コレクション「%1$s」のタイトルが空白です。 + 「%2$s」のフォルダー %1$d のIDが空白です。 + 「%2$s」のフォルダー「%1$s」のタイトルが空白です。 + フォルダー「%2$s」のソース %1$d にフィールドが空白のものがあります。 + フォルダー「%2$s」のソース %1$d にTraktリストIDがありません。 + 無効なJSON:%1$s + アドオンが見つかりません:%1$s + Traktの映画リスト + Traktのシリーズリスト + 1月 + 2月 + 3月 + 4月 + 5月 + 6月 + 7月 + 8月 + 9月 + 10月 + 11月 + 12月 + 1月 + 2月 + 3月 + 4月 + 5月 + 6月 + 7月 + 8月 + 9月 + 10月 + 11月 + 12月 + 制作会社 + ネットワーク + %1$s を読み込めませんでした + 人気 + 新着 + %1$s • %2$s + 高評価 + レーティング + 映画の詳細 + 原語 + 制作国 + 公開情報 + 上映時間 + ポスター + テキスト + シリーズの詳細 + ステータス + 動画 + ファイル + 直接のストリームリンクがありません + 前のダウンロードを置き換えました + ダウンロードを開始しました + このストリーム形式はダウンロードに対応していません + 空のレスポンスボディ + ダウンロードファイルの確定に失敗しました + リクエストが HTTP %1$d で失敗しました + ダウンロードシステムが初期化されていません + 一時ダウンロードファイルを開けませんでした + 一時ダウンロードファイルが開いていません + ダウンロードリクエストに失敗しました + 一時ダウンロードファイルへの書き込みに失敗しました + %1$s - %2$s + 詳細画面で保存をタップしたタイトルがここに表示されます。 + ライブラリは空です + ライブラリを読み込めませんでした + その他 + クラウド + 保存済み + ライブラリ + Traktに連携してウォッチリストや個人リストにタイトルを保存してください。 + Traktライブラリは空です + Traktライブラリを読み込めませんでした + Traktライブラリ + アカウントを連携 + クラウドライブラリのファイルを参照するには、「連携サービス」設定でアカウントを連携してください。 + クラウドアカウントが連携されていません + 連携サービスを開く + 連携済みアカウントのファイルを参照するには、「連携サービス」設定でクラウドライブラリを有効にしてください。 + クラウドライブラリがオフです + 現在のフィルターに一致する再生可能なクラウドファイルがありません。 + まだありません + 再生するファイルを選択 + %1$s クラウドライブラリを読み込めませんでした + このアイテムには再生可能な動画ファイルがありません。 + 再生可能なファイルがありません + 再生可能なファイルがありません + クラウドライブラリがオフです。 + このクラウドファイルを再生できませんでした。 + ファイルを再生 + クラウドサービスが連携されていません。 + %1$s が連携されていません。 + %1$d 件の再生可能ファイル + すべて + クラウドライブラリを更新 + プロバイダーを選択 + タイプを選択 + 再生準備完了 + すべて + トレント + Usenet + Web + ファイル + アニメ + チャンネル + 映画 + シリーズ + TV + %1$s が配信されました + %1$s • %2$s が配信されました + 新しいエピソードが配信されました + %1$s が配信されました + エピソード公開 + アルコール/薬物 + 恐怖シーン + ヌード + 不適切な言葉 + 軽度 + 中程度 + 強度 + 暴力 + 制作 + 監督 + 脚本 + オーディエンススコア + 再生可能な予告編ストリームが見つかりません。 + シーズン %1$d - %2$s + B + KB + MB + GB + イントロを投稿 + 映像設定 + %1$s ではクラウドライブラリが利用できません。 + 映像 + 調整をリセット + 出力プリセット + HDRピーク検出 + メタデータが不正または欠落している場合にHDRのピーク輝度を推定します。 + トーンマッピング + デバンド + わずかなパフォーマンスコストで色の帯状ノイズを軽減します。 + フレーム補間 + mpvがディスプレイ同期をうまく使用できる場合にモーションをスムーズにします。 + 明るさ + コントラスト + 彩度 + ガンマ + ネイティブEDR + HDR対応のiPhoneとiPadに最適。 + SDRトーンマッピング + SDRスタイルの出力でより予測可能な白と黒。 + 互換性 + 旧iOS MPVの動作に最も近い。 + カスタム + 下の詳細設定値を使用します。 + オフ + iOS映像出力 + ハードウェアデコーダー + 拡張ダイナミックレンジ + 新しい再生セッションのデフォルトMetal出力モード。 + ディスプレイカラーヒント + mpvがデフォルトでアクティブなディスプレイの色空間をターゲットにするようにします。 + ターゲット原色 + ターゲット転送 + iOS音声出力 + 音声出力 + AVFoundationを試み、失敗した場合はAudioUnitにフォールバックします。 + 空間オーディオとマルチチャンネル出力の試験的サポート。 + レガシーのAudioUnit出力を使用します。 + 結果の管理 + 最大結果数 + 表示する結果数を制限します。 + 結果の並び順 + 結果の並び順を選択します。 + 解像度ごとの上限 + 並び替え後の2160p・1080p・720pの重複結果を制限します。 + 品質ごとの上限 + 並び替え後のBluRay・WEB-DL・REMUX の重複結果を制限します。 + サイズ範囲 + ファイルサイズで結果をフィルターします。 + 詳しく見る + デフォルト形式 + オリジナル形式 + 1行に1グループずつ入力してください。 + オリジナル順 + 最高品質優先 + ファイルサイズ大優先 + ファイルサイズ小優先 + 最高音質優先 + 言語優先 + すべて + %1$d 件選択中 + すべての結果 + %1$d 件の結果 + 最大 %1$dGB + %1$dGB 以上 + %1$d〜%2$dGB + 優先解像度 + 選択した解像度をデフォルト順で先頭に並べます。 + 必須解像度 + 選択した解像度のみ表示します。 + 除外する解像度 + 選択した解像度を非表示にします。 + 優先品質 + 選択した品質をデフォルト順で先頭に並べます。 + 必須品質 + 選択した品質のみ表示します。 + 除外する品質 + 選択した品質を非表示にします。 + 優先映像タグ + DV・HDR・10bit・IMAXなどのタグを並べ替えます。 + 必須映像タグ + DV・HDR・10bit・IMAX・SDRなどのタグを必須にします。 + 除外する映像タグ + DV・HDR・10bit・3Dなどのタグを非表示にします。 + 優先音声タグ + Atmos・TrueHD・DTS・AACなどのタグを並べ替えます。 + 必須音声タグ + Atmos・TrueHD・DTS・AACなどのタグを必須にします。 + 除外する音声タグ + 選択した音声タグを非表示にします。 + 優先チャンネル + 優先するチャンネルレイアウトを先頭に並べます。 + 必須チャンネル + 選択したチャンネルレイアウトのみ表示します。 + 除外するチャンネル + 選択したチャンネルレイアウトを非表示にします。 + 優先エンコード + AV1・HEVC・AVCなどのエンコードを並べ替えます。 + 必須エンコード + AV1・HEVC・AVCなどのエンコードを必須にします。 + 除外するエンコード + 選択したエンコードを非表示にします。 + 優先言語 + 優先する音声言語を先頭に並べます。 + 必須言語 + 選択した言語を含む結果のみ表示します。 + 除外する言語 + すべての言語が除外対象の結果を非表示にします。 + 必須リリースグループ + 選択したリリースグループのみ表示します。 + 除外するリリースグループ + 選択したリリースグループを非表示にします。 + タイムスタンプを投稿 + セグメントタイプ + イントロ + 振り返り + アウトロ + 開始時刻 (MM:SS) + 終了時刻 (MM:SS) + 投稿 + キャプチャ + ハードウェアデコーダー + 音声出力 + ターゲット原色 + ターゲット転送 + 解決済みTraktリスト + TMDBディスカバー + US, KR, JP, IN + Traktリスト名、URL、またはIDを入力してください + 有効なTMDB IDまたはURLを入力してください。 + TMDBソースを読み込めませんでした + TraktリストIDまたはURLを入力してください + Traktリストを読み込めませんでした + カナダ + オーストラリア + ドイツ + 映画 + シリーズ + クラウドライブラリが無効です。 + APIキーが無効か接続に失敗しました + マニフェストに「%1$s」がありません + TMDBコレクション %1$s + TMDB監督 %1$s + TMDBディスカバー + 有効なTMDB IDまたはURLを入力してください。 + TMDBリスト %1$s + TMDBネットワーク %1$s + TMDB人物 %1$s + TMDB制作会社 %1$s + TMDBソースを読み込めませんでした + ID %1$s + TMDBソースを使用するには設定でTMDB APIキーを追加してください。 + TMDBコレクションが見つかりません + TMDB会社が見つかりません + TMDBディスカバーがデータを返しませんでした + TMDBリストが見つかりません + TMDBコレクションIDがありません + TMDBリストIDがありません + TMDB人物IDがありません + TMDBネットワークが見つかりません + TMDBの人物クレジットが見つかりません + TMDB人物が見つかりません + Traktの認証情報がありません。 + %1$s (%2$d) + 有効なTraktリストIDまたはURLを入力してください + %1$d 件 + %1$d いいね + Traktリストが見つからないか、公開されていません + TraktリストIDがありません + Traktリストに数値IDが含まれていませんでした + Traktの公開リスト + Traktのレート制限に達しました + Traktリクエストに失敗しました + Traktコメントの読み込みに失敗しました (%1$d) + %1$d 時間 %2$d 分 + %1$d 時間 + %1$d 分 + ダウンロードファイルの確定に失敗しました + 一時ダウンロードファイルを開けませんでした + 一時ダウンロードファイルが開いていません + 一時ダウンロードファイルへの書き込みに失敗しました + Nuvioライブラリ + 接続の問題 + 空のレスポンスボディ + 接続を確認して再試行してください。 + リクエストが HTTP %1$d で失敗しました + %1$s (%2$s) + MPVプレーヤーエンジンが利用できません。アプリを再ビルドしてください。 + このストリームを再生できません。 + プラグイン無効 + プラグイン有効 + %1$d プロバイダー + 更新中 + %1$d リポジトリ + TMDB APIキー未設定 + TMDB APIキー設定済み + プラグインリポジトリをインストール + インストール中… + プロバイダーをテスト + テスト中… + プラグインリポジトリを削除 + プラグインリポジトリを更新 + 利用可能なプロバイダーがまだありません。 + リポジトリURLを追加してストリーム探索用のプロバイダープラグインをインストールしてください。 + プラグインリポジトリがまだインストールされていません。 + ストリーム探索中にプラグインプロバイダーを使用します。 + プラグインプロバイダーをグローバルで有効にする + そのプラグインリポジトリはすでにインストールされています。 + プラグインリポジトリのURLを入力してください。 + 有効なプラグインURLを入力してください。 + プラグインリポジトリをインストールできません + プロバイダーが見つかりません + リポジトリを更新できません + このビルドではプラグインが利用できません。 + ストリームでソースごとではなくリポジトリごとに1つのプロバイダーを表示します。 + リポジトリごとにプラグインプロバイダーをグループ化する + プラグインマニフェストURL + マニフェストに名前がありません。 + マニフェストにプロバイダーがありません。 + マニフェストにバージョンがありません。 + マニフェストに名前がありません。 + マニフェストにプロバイダーがありません。 + マニフェストにバージョンがありません。 + %1$s をインストールしました。 + リポジトリにより無効 + 説明なし + v%1$s + プラグインリポジトリ + バージョン %1$s + そのプラグインリポジトリはすでにインストールされています。 + プラグインリポジトリをインストールできません + リポジトリを更新できません + リポジトリを追加 + インストール済みリポジトリ + 概要 + プロバイダー + エラー + プロバイダーのテストに失敗しました + テスト結果 (%1$d 件) + プラグインプロバイダーにはTMDB APIキーが必要です。TMDB画面でAPIキーを設定しないと、プラグインプロバイダーが正常に動作しない場合があります。 + %1$s の検索結果が返されませんでした。 + APIキーが無効か接続に失敗しました + プラグインリポジトリ + イントロを投稿 + 投稿 + キャプチャ + 終了時刻 (MM:SS) + セグメントタイプ + 開始時刻 (MM:SS) + Traktに連携しました + Traktの連携を解除しました + TB + リリースにAPKアセットが見つかりません + ダウンロードが HTTP %1$d で失敗しました + ダウンロードした更新ファイルが見つかりません。 + ダウンロードボディが空です + GitHub releases APIエラー:%1$d + まだアップデートが公開されていません。 + リリースにタグまたは名前がありません + %1$s に放送 + 本日放送 + 明日放送 + + %1$d 日後に放送 + + %1$s + 今日 + 明日 + + %1$d 日後 + + 新しいエピソード + 新シーズン + Traktリスト %1$s + Androidシステムプレーヤー + P2Pストリーミング + このストリームはピアツーピア(P2P)技術を使用しています。P2Pを有効にすることで、以下の事項に同意したものとみなされます:\n\n• お使いのIPアドレスがネットワーク内の他のピアに表示されます\n• アクセスするコンテンツに関する責任はすべてあなたにあります\n• あなたの管轄地域においてこのコンテンツをストリーミングする合法的な権利を有することを確認します\n• Nuvioはいかなるコンテンツもホスト、配信、管理しません\n• P2Pストリーミングの使用から生じる法的結果についてNuvioは一切の責任を負いません\n\nこの機能の使用はすべてご自身の責任において行ってください。P2Pはいつでも設定で無効にできます。 + P2Pを有効にする + キャンセル + 不明なトレントエラー + P2Pストリーミング + ピアツーピア(トレント)ストリームを許可する + トレント統計を非表示 + 読み込み中および再生中のバッファ・シード数・ピア数・ダウンロード速度を非表示にする + %1$d シード · %2$d ピア + %1$s バッファ済み · %2$s · %3$s + %1$s · %2$s + %1$d ピア · %2$d シード · %3$d%% + ピアに接続中… + P2Pエンジンを起動中… + トレントの開始に失敗しました:%1$s + トレントエラー:%1$s + From 0bfd0869171dc33fe5f1556ffbe1c401989b031e Mon Sep 17 00:00:00 2001 From: VenusIsJaded Date: Mon, 22 Jun 2026 07:16:03 -0500 Subject: [PATCH 9/9] fix: ExoPlayer unpausing when leaving the app and coming back The LifecycleEventObserver was only capturing playWhenReady at the time the effect ran. When ON_START fired later, it would use the old value and resume playback even if the user had paused before leaving the app. Use rememberUpdatedState so we always read the live value. Fixes #1080 --- .../com/nuvio/app/features/player/PlayerEngine.android.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt index 3ec9539cb..97911e9c3 100644 --- a/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt +++ b/composeApp/src/androidMain/kotlin/com/nuvio/app/features/player/PlayerEngine.android.kt @@ -90,6 +90,7 @@ actual fun PlatformPlayerSurface( val lifecycleOwner = LocalLifecycleOwner.current val latestOnSnapshot = rememberUpdatedState(onSnapshot) val latestOnError = rememberUpdatedState(onError) + val latestPlayWhenReady = rememberUpdatedState(playWhenReady) val coroutineScope = rememberCoroutineScope() val playerSettings = remember { @@ -385,7 +386,7 @@ actual fun PlatformPlayerSurface( val activity = context.findActivity() val observer = LifecycleEventObserver { _, event -> when (event) { - Lifecycle.Event.ON_START -> exoPlayer.playWhenReady = playWhenReady + Lifecycle.Event.ON_START -> exoPlayer.playWhenReady = latestPlayWhenReady.value Lifecycle.Event.ON_STOP -> { val isInPictureInPicture = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true @@ -405,7 +406,7 @@ actual fun PlatformPlayerSurface( } LaunchedEffect(exoPlayer, playWhenReady) { - exoPlayer.playWhenReady = playWhenReady + exoPlayer.playWhenReady = latestPlayWhenReady.value syncPlayerViewKeepScreenOn() latestOnSnapshot.value(exoPlayer.snapshot()) }