Compare commits
5 Commits
a44bdd7287
...
76daf8e32c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76daf8e32c | ||
|
|
247e58e4b8 | ||
|
|
5edc117ed3 | ||
|
|
2de2aa7891 | ||
|
|
f9b7c53142 |
@ -1,750 +0,0 @@
|
||||
{
|
||||
"signInWithGoogle": "Iniciar sesión con Google",
|
||||
"or": "O",
|
||||
"signInWithYourAccount": "Inicia sesión con tu cuenta",
|
||||
"signInWithApple": "Iniciar sesión con Apple",
|
||||
"loginRepresentsAgreementTo": "Iniciar sesión representa un acuerdo para",
|
||||
"termsofService": "Términos de servicio",
|
||||
"privaceyPolicy": "política de privacidad",
|
||||
"tips": "Consejos",
|
||||
"searchNoDataTips": "Ingrese la sala o ID de usuario que desea buscar.",
|
||||
"games": "Juegos",
|
||||
"mine": "Mío",
|
||||
"official": "Oficial",
|
||||
"party": "Fiesta",
|
||||
"other": "Otro",
|
||||
"yes": "Sí",
|
||||
"bag": "Bolsa",
|
||||
"sound2": "Sonido",
|
||||
"startYourBrandNewJourney": "Comience su nuevo viaje",
|
||||
"maliciousHarassment": "Acoso malicioso",
|
||||
"event": "Evento",
|
||||
"roomEdit": "Editar habitación",
|
||||
"cancelRoomPassword": "¿Estás seguro de que quieres eliminar la contraseña de la sala?",
|
||||
"roomMemberFee": "Tarifa de miembro de la sala",
|
||||
"roomTheme2": "Tema de la habitación",
|
||||
"background": "Fondo",
|
||||
"blockedList2": "Lista de bloqueados",
|
||||
"roomPassword": "Contraseña de la habitación",
|
||||
"numberOfMic": "Número de micrófono",
|
||||
"pleaseEnterContent": "Por favor ingresa el contenido",
|
||||
"profilePhoto": "Foto de perfil",
|
||||
"aboutMe": "Acerca de mí",
|
||||
"myRoom": "mi cuarto",
|
||||
"noHistoricalRecordsAvailable": "No hay registros históricos disponibles.",
|
||||
"inviteNewUsersToEarnCoins": "Invita a nuevos usuarios a ganar monedas.",
|
||||
"crateMyRoom": "CREA TU PROPIA HABITACIÓN.",
|
||||
"casualInteraction": "Interacción casual",
|
||||
"haveGamePlayingTips": "Tienes un juego en progreso. Por favor sal del juego actual. ¿Estás seguro de que quieres salir?",
|
||||
"historicalTour": "Tour Histórico",
|
||||
"gameCenter": "Centro de juegos",
|
||||
"returnToVoiceChat": "¿Volver al chat de voz?",
|
||||
"exitGameMode": "Salir del modo de juego",
|
||||
"enterTheRoom": "Entra a la habitación",
|
||||
"invite": "Invitar",
|
||||
"recent": "Reciente",
|
||||
"popularEvents": "Eventos populares",
|
||||
"inviteGoRoomTips": "Siempre aquí para ti, llueva o haga sol. ¡Entra y saluda!",
|
||||
"confirmInviteThisUserToTheRoom": "¿Confirmar la invitación de este usuario (ID:{1}) a la sala?",
|
||||
"clearCacheSuccessfully": "Borrar caché exitosamente",
|
||||
"sent": "Enviado",
|
||||
"keep": "Mantener",
|
||||
"open": "Abierto",
|
||||
"deleteAccountTips2": "*Si cambia de opinión, puede volver a iniciar sesión en su cuenta actual dentro de los siete días y la restauraremos automáticamente. Si no se produce ninguna restauración dentro de los siete días, la cuenta se eliminará permanentemente.",
|
||||
"deleteAccountTips": "Tiene todos los derechos administrativos sobre esta cuenta. Si tiene la intención de eliminar la cuenta, tenga en cuenta los siguientes riesgos asociados con esta operación:\n1. Una vez que la cuenta se elimine correctamente, ya no podrá iniciar sesión en su cuenta actual. La eliminación de la cuenta es una acción permanente.\n2. Una vez que la cuenta se elimine correctamente, no podrá recuperar ningún dato de la cuenta. Toda la información (incluidas habitaciones y amigos), moneda virtual, regalos y artículos virtuales se eliminarán permanentemente y no se podrán restaurar.\n3. Período de reflexión: si no restaura la cuenta, no podrá acceder a la página de compra, a la página de retiro ni a ninguna otra página de la aplicación.\n4. Durante el período de reflexión o después de que la cuenta haya sido eliminada, la página de perfil indicará que se ha eliminado. Para proteger su cuenta de que otros busquen o accedan a ella, su información personal se eliminará de los sistemas relacionados con las funciones diarias. Cuando la eliminación de la cuenta implique seguridad nacional, procedimientos civiles o penales, o la protección de derechos e intereses legítimos de terceros, el equipo oficial se reserva el derecho de rechazar la solicitud de eliminación de la cuenta del usuario.\nSi está seguro de que desea eliminar todos los datos personales de su cuenta actual, haga clic en \"Eliminar cuenta\".",
|
||||
"accountDeletionNotice": "Aviso de eliminación de cuenta:",
|
||||
"thisUserHasBeenBlacklisted": "Este usuario ha sido incluido en la lista negra.",
|
||||
"trend": "Tendencia",
|
||||
"like": "Como",
|
||||
"more": "Más",
|
||||
"discard": "Desechar",
|
||||
"catchFirstComment": "Captura el primer comentario.",
|
||||
"reply": "Responder",
|
||||
"posting": "Destino",
|
||||
"multiple": "Múltiple",
|
||||
"successfullyRemovedFromTheBlacklist": "¡Eliminado con éxito de la lista negra!",
|
||||
"successfullyAddedToTheBlacklist": "¡Agregado exitosamente a la lista negra!",
|
||||
"youAreCurrentlyCPRelationshipPleaseDissolve": "Actualmente estás en una relación CP.\nPor favor disuélvalo primero.",
|
||||
"areYouSureToCancelBlacklist": "¿Estás seguro de cancelar la lista negra?",
|
||||
"areYouSureYouWantToBlockThisUser": "¿Estás seguro de que quieres bloquear a este usuario?",
|
||||
"removeFromBlacklist": "Eliminar de la lista negra",
|
||||
"moveToBlacklist": "Mover a la lista negra",
|
||||
"userBlacklist": "Lista negra de usuarios",
|
||||
"specialEffectsManagement": "Manejo de efectos especiales",
|
||||
"wishingYouHappinessEveryDay": "Deseándote felicidad todos los días.",
|
||||
"newMessage": "Nuevo mensaje",
|
||||
"createRoomSuccsess": "¡La habitación se creó correctamente!",
|
||||
"contactUs": "Contáctenos",
|
||||
"systemAnnouncementTips1": "Protéjase contra el fraude:",
|
||||
"systemAnnouncementTips": "Verificar información únicamente a través de canales oficiales. Nunca descargue software de terceros, no comparta datos personales ni transfiera dinero en función de solicitudes externas. Las identificaciones oficiales del personal son solo 10000, 10003 y 10086. Ante cualquier sospecha, deténgase e informe a través de",
|
||||
"systemAnnouncement": "Anuncio del sistema",
|
||||
"doNotClickUnfamiliarTips": "No haga clic en enlaces desconocidos, ya que pueden exponer su información personal. Nunca compartas tu DNI o los datos de tu tarjeta bancaria con nadie.",
|
||||
"atTag": "@Etiqueta",
|
||||
"sayHi2": "Di hola",
|
||||
"roomBottomGreeting": "Hola...",
|
||||
"canSendMsgTips": "Ambas partes deben seguirse antes de poder enviar mensajes privados.",
|
||||
"msgSendRedEnvelopeTips": "*Se cobrará una tarifa de servicio del 10 % en los sobres rojos y los destinatarios solo recibirán el 90 % del valor del sobre rojo. El nivel de riqueza del remitente debe ser superior al nivel 10.",
|
||||
"reapply": "Aplicar de nuevo",
|
||||
"cancelRequest": "Cancelar solicitud",
|
||||
"pending": "Pendiente",
|
||||
"supporter": "Seguidor",
|
||||
"coinsReceived": "Monedas recibidas",
|
||||
"numberOfSign": "Número de signo: {1}",
|
||||
"hostWeeklyRank": "Clasificación semanal de anfitrión",
|
||||
"supporterWeeklyRank": "Clasificación semanal de partidario",
|
||||
"memberList": "Lista de miembros",
|
||||
"treasureChest": "Cofre del tesoro",
|
||||
"applicationRecord": "Registro de solicitud",
|
||||
"appUpdateTip": "La aplicación tiene una nueva versión ({1}). ¿Quieres descargarlo ahora?",
|
||||
"ownerIncomeCoins": "Ingresos del propietario: {1} monedas",
|
||||
"game": "Juego",
|
||||
"skip2": "Saltar",
|
||||
"skipCountdown": "Saltar {1}",
|
||||
"coins4": "monedas",
|
||||
"weekStart": "Inicio de semana",
|
||||
"forMoreRewardsPleaseCheckTheTaskCenter": "Para obtener más recompensas, consulte el centro de tareas.",
|
||||
"kingQuuen": "Rey-Reina",
|
||||
"ramadan": "Ramadán",
|
||||
"updateNow": "Actualizar ahora",
|
||||
"allGames": "Todos los juegos",
|
||||
"fishClass": "Clase de pescado",
|
||||
"greedyClass": "Clase codiciosa",
|
||||
"raceSeries": "Serie de carreras",
|
||||
"slotsClass": "Clase de tragamonedas",
|
||||
"others": "Otros",
|
||||
"hotGames": "Juegos calientes",
|
||||
"chatBox": "cuadro de chat",
|
||||
"termsOfServicePrivacyPolicyTips": "Al continuar, aceptas los Términos de servicio y la Política de privacidad.",
|
||||
"and": " y ",
|
||||
"pleaseSelectTheTypeContent": "Seleccione el tipo de contenido ofensivo.",
|
||||
"illegalInformation": "Información ilegal",
|
||||
"inappropriateContent": "Contenido inapropiado",
|
||||
"personalAttack": "Ataque personal",
|
||||
"confirm": "Confirmar",
|
||||
"spam": "Correo basura",
|
||||
"countdownMinutes": "Minutos de cuenta regresiva:",
|
||||
"number2": "Número:",
|
||||
"fraud": "Fraude",
|
||||
"received": "Recibió",
|
||||
"currentProgress": "Progreso actual",
|
||||
"currentStage": "Etapa actual:{1}",
|
||||
"roomReward2": "Recompensa de habitación:{1}",
|
||||
"roomReward": "Recompensa de habitación",
|
||||
"expirationTime": "tiempo de vencimiento",
|
||||
"ownerSendTheRedEnvelope": "El propietario envió monedas de recompensa.",
|
||||
"rewardCoins": "Monedas de recompensa:{1} monedas",
|
||||
"signInRewardReceived": "Iniciaste sesión exitosamente. Recompensa: {1}",
|
||||
"registerRewardReceived": "Recompensa de registro recibida: {1}",
|
||||
"lastWeekProgress": "El progreso de la semana pasada.",
|
||||
"redEnvelopeTips2": "*Si el sobre rojo no se reclama dentro del plazo, las monedas restantes serán devueltas al usuario que envió el sobre rojo.",
|
||||
"goToRecharge": "ir a recargar",
|
||||
"deleteAccount2": " Eliminar cuenta ({1}s)",
|
||||
"areYouSureYouWantToDeleteYourAccount": " ¿Estás seguro de que quieres eliminar tu cuenta?",
|
||||
"insufhcientGoldsGoToRecharge": "Oro insuficiente. ¡Recarga ahora!",
|
||||
"coins2": "{1} monedas",
|
||||
"remainingNumberTips": "Disponible restante: ({1}/{2})",
|
||||
"collectionTimeTips": "Hora de recogida:{1}({2}/{3})",
|
||||
"sendARedEnvelope": "enviar un sobre rojo",
|
||||
"sendRedPackConfirmTips": "¿Estás seguro de que quieres enviar el paquete rojo?",
|
||||
"redEnvelopeSendingRecords": "Registros de envío de sobre rojo:",
|
||||
"redEnvelope": "Sobre rojo",
|
||||
"roomRedPacketGoldCoins": "Monedas de oro",
|
||||
"roomRedPacketQuantity": "Cantidad",
|
||||
"roomRedPacketPickupTime": "Hora de recogida",
|
||||
"roomRedPacketInMinutes": "En {1} minutos",
|
||||
"roomRedPacketClaimNow": "Reclamar ahora",
|
||||
"roomRedPacketSendLuckyPack": "Envía un paquete de la suerte",
|
||||
"roomRedPacketSending": "Envío...",
|
||||
"roomRedPacketSendSuccess": "Paquete de la suerte enviado",
|
||||
"roomRedPacketComingSoon": "Muy pronto",
|
||||
"roomRedPacketRulesTitle": "Normas",
|
||||
"roomRedPacketRulesContent": "1. Puedes gastar monedas para enviar un Lucky Pack. Si otras personas abren tu Lucky Pack, pueden recibir una cantidad aleatoria de monedas.\n\n2. Dos horas después de enviar un Lucky Pack, las monedas no reclamadas se devolverán a su billetera.\n\n3. Después de enviar un Lucky Pack a todo el servidor, se activará un banner de Lucky Pack para todo el servidor, lo que le brindará más atención.\n\n4. Puedes enviar Lucky Packs de habitación a %1$s por día.\n\n5. Los Lucky Packs enviados por usuarios fuera de su región no se pueden reclamar.",
|
||||
"roomRedPacketHistoryTitle": "Historial de entrega del paquete de la suerte",
|
||||
"roomRedPacketHistoryItemTitle": "Bolsa de la suerte ({1} sobres rojos)",
|
||||
"roomRedPacketHistoryStatusSent": "Enviado",
|
||||
"roomRedPacketHistoryStatusFinished": "Totalmente reclamado",
|
||||
"roomRedPacketHistoryStatusPending": "Pendiente",
|
||||
"roomRedPacketHistoryStatusReturned": "Devuelto",
|
||||
"roomRedPacketHistoryRefund": "Reembolso: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "Registros de reclamaciones",
|
||||
"roomRedPacketChatMessage": "Te envié un sobre rojo, ¡ábrelo ahora mismo!",
|
||||
"roomRedPacketReceivedMessage": "Reclama {1} monedas del {2} Lucky Pack",
|
||||
"roomRedPacketOpenSubtitle": "El paquete de la suerte ha llegado",
|
||||
"roomRedPacketOpenYouGot": "obtienes",
|
||||
"roomRedPacketClaimSuccessTips": "Las monedas han sido enviadas a tu billetera.",
|
||||
"roomRedPacketOpenSoldOut": "El Lucky Pack ya se ha agotado.",
|
||||
"roomRedPacketOpenExpired": "El paquete de la suerte ha caducado.",
|
||||
"roomRedPacketRegionNotMatch": "Solo los usuarios de la misma región lingüística pueden reclamar este paquete de la suerte.",
|
||||
"roomRedPacketNotStarted": "El paquete de la suerte aún no está listo.",
|
||||
"roomRedPacketDailyLimitExceeded": "Se ha alcanzado el límite de envío de hoy.",
|
||||
"roomRedPacketInsufficientBalance": "Monedas de oro insuficientes.",
|
||||
"roomRedPacketPresenceRequired": "Quédese en la habitación para usar paquetes de la suerte.",
|
||||
"roomRedPacketNetworkError": "No se puede procesar el paquete de la suerte en este momento.",
|
||||
"roomRedPacketAlreadyClaimed": "Ya has reclamado este paquete de la suerte.",
|
||||
"roomRedPacketOpenWaiting": "Se puede abrir en {1} minutos.",
|
||||
"roomRedPacketOpenWaitingCountdown": "Disponible para recogida después del {1}",
|
||||
"roomRedPacketOkay": "Bueno",
|
||||
"roomRedPacketFloatingTitle": "{1} envió un sobre rojo",
|
||||
"roomRedPacketFloatingSubtitle": "Envió un sobre rojo",
|
||||
"redEnvelopeRecTips2": "Todos los sobres rojos han sido reclamados.",
|
||||
"redEnvelopeRecTips3": "¡El tiempo de recogida del sobre rojo ha expirado!",
|
||||
"openTheTreasureChest": "Abre el cofre del tesoro",
|
||||
"redEnvelopeRecTips1": "Las monedas ganadas se han depositado en su billetera.",
|
||||
"redEnvelopeTips1": "Monedas:",
|
||||
"roomTools": "Herramientas de la habitación:",
|
||||
"entertainment": "Entretenimiento:",
|
||||
"reportSucc": "Informe exitoso",
|
||||
"pornography": "Pornografía",
|
||||
"reportInputTips": "Describa el problema con el mayor detalle posible para que podamos comprenderlo y resolverlo.",
|
||||
"cancel": "Cancelar",
|
||||
"join": "Unirse",
|
||||
"items": "Elementos",
|
||||
"vistors": "Visitantes",
|
||||
"fans": "aficionados",
|
||||
"balanceNotEnough": "Saldo insuficiente de monedas de oro. ¿Quieres ir a recargar?",
|
||||
"skip": "Saltar {1}",
|
||||
"letGoToWatch": "¡Vamos a mirar!",
|
||||
"launchedARocket": "lanzó un cohete",
|
||||
"sendUserId": "Enviar ID de usuario:{1}",
|
||||
"giveUpIdentity": "renunciar a la identidad",
|
||||
"leaveRoomIdentityTips": "¿Estás seguro de que quieres ceder la identidad de la habitación?",
|
||||
"joinMemberTips2": "¿Quieres confirmar unirte a la sala como miembro?",
|
||||
"sureUnfollowThisRoom": "¿Seguro que dejarás de seguir esta sala?",
|
||||
"welcomeMessage": "¡Bienvenido a nuestra aplicación, {nombre}!",
|
||||
"settings": "Ajustes",
|
||||
"account": "Cuenta",
|
||||
"common": "Común",
|
||||
"delete": "Borrar",
|
||||
"copy": "Copiar",
|
||||
"bio": "Biografía",
|
||||
"introduction": "Introducción",
|
||||
"useCoupontips": "¿Estás seguro de que quieres utilizar el cupón?",
|
||||
"searchUserId": "Buscar ID de usuario",
|
||||
"sendUser": "Enviar usuario",
|
||||
"hobby": "Pasatiempo",
|
||||
"sendCoupontips": "¿Estás seguro de que quieres enviar este cupón a este usuario?",
|
||||
"youDontHaveAnyCouponsYet": "Aún no tienes ningún cupón.",
|
||||
"recall": "Recordar",
|
||||
"youHaventFollowed": "No has seguido ninguna habitación",
|
||||
"deleteFromMyDevice": "Eliminar de mi dispositivo",
|
||||
"deleteOnAllDevices": "Eliminar en todos los dispositivos",
|
||||
"messageHasBeenRecalled": "Este mensaje ha sido recordado.",
|
||||
"recallThisMessage": "¿Recuerdas este mensaje?",
|
||||
"language": "Idioma",
|
||||
"feedback": "Comentario",
|
||||
"signedin": "Iniciado sesión",
|
||||
"receiveSucc": "Reclamado con éxito",
|
||||
"about": "Acerca de",
|
||||
"aboutUs": "Sobre nosotros",
|
||||
"theme": "Tema",
|
||||
"wealthLevel": "Nivel rico",
|
||||
"userLevel": "Nivel de encanto",
|
||||
"goToUpload": "ir a subir",
|
||||
"logout": "Cerrar sesión",
|
||||
"luck": "Suerte",
|
||||
"level": "Nivel",
|
||||
"themeGoToUploadTips": "1. La revisión se completará dentro de las 24 horas posteriores a la carga exitosa.\n2. Todas las monedas serán devueltas si la revisión falla.",
|
||||
"pleaseUploadAccordingToExample": "Cárguelo de acuerdo con el ejemplo proporcionado.",
|
||||
"home": "Hogar",
|
||||
"explore": "Explorar",
|
||||
"me": "A mí",
|
||||
"socialPrivilege": "privilegio social",
|
||||
"information": "Información",
|
||||
"myPhoto": "mi foto",
|
||||
"areYouSureYouWantToSpend3": "*Si la otra parte rechaza la invitación de CP, sus monedas serán devueltas a su billetera.",
|
||||
"areYouSureYouWantToSpend": "¿Estás seguro de que quieres gastar?",
|
||||
"areYouSureYouWantToSpend2": "enviar una invitación CP a este usuario?",
|
||||
"underReview": "En revisión",
|
||||
"doYouWantToDeleteIt": "¿Quieres eliminarlo?",
|
||||
"chooseFromAblum": "Elige del álbum",
|
||||
"spaceBackground": "Fondo espacial",
|
||||
"editProfile": "Editar perfil",
|
||||
"sendTheCpRequest": "Enviar la solicitud de CP",
|
||||
"addCp": "Agregar CP",
|
||||
"partWays": "Caminos parciales",
|
||||
"reconcile": "Conciliar",
|
||||
"separated": "Apartado",
|
||||
"areYouSureYouWantToSpend5": "{1} te confesó el sentimiento; si aceptáis, os convertiréis en pareja.",
|
||||
"areYouSureYouWantToSpend6": "{1} quiere volver contigo. Si decide conciliar, se restaurarán todos sus datos anteriores.",
|
||||
"reconcileInvitationTips": "*Si la otra parte rechaza la invitación de CP, sus monedas serán devueltas a su billetera.",
|
||||
"reconcileInvitation": "Conciliar invitación",
|
||||
"areYouSureYouWantToSpend4": "enviar una invitación de reconciliación a este usuario?",
|
||||
"partWaysTips": "*Si un miembro de una pareja decide separarse, habrá un período de reflexión de 7 días. Durante este tiempo, ambas partes pueden optar por reconciliarse y todos los datos se restaurarán tras la reconciliación. Si no se elige ninguna reconciliación al final del período de 7 días, se borrarán los datos de la pareja.",
|
||||
"areYouSureYouWantToPartWaysWithYourCP": "¿Estás seguro de que quieres separarte de tu CP?",
|
||||
"timeSpentTogether": "Tiempo que pasamos juntos: {1} días",
|
||||
"firstDay": "Primer día:{1}",
|
||||
"numberOfMyCPs": "Número de mis CP:({1}/{2})",
|
||||
"props": "Accesorios",
|
||||
"win": "Ganar",
|
||||
"dice": "Dados",
|
||||
"rps": "RPS",
|
||||
"operationFail": "La operación fracasó.",
|
||||
"enterRoomFailedRetry": "No se pudo entrar a la habitación. Por favor inténtalo de nuevo.",
|
||||
"voiceConnectionFailedRetry": "La conexión de voz falló. Por favor inténtalo de nuevo.",
|
||||
"likedYourComment": "Me gustó tu comentario.",
|
||||
"doYouWantToKeepTheDraft": "¿Quieres conservar el borrador?",
|
||||
"operationsAreTooFrequent": "Las operaciones son demasiado frecuentes.",
|
||||
"luckNumber": "Número de suerte",
|
||||
"relationShip": "Relación",
|
||||
"couple": "Pareja {1}:",
|
||||
"couple2": "Pareja",
|
||||
"reject": "Rechazar",
|
||||
"accept": "Aceptar",
|
||||
"noMatchedCP": "No hay CP coincidente",
|
||||
"inviteYouToBecomeBD": "Te invitamos a convertirte en BD.",
|
||||
"adminInviteRechargeAgent": "Te invitamos a convertirte en agente de Recarga.",
|
||||
"confirmAcceptTheInvitation": "¿Confirmar para aceptar la invitación?",
|
||||
"confirmDeclineTheInvitation": "¿Confirmar que rechazas la invitación?",
|
||||
"cpInviteRejectedByOther": "La otra parte se negó a convertirse en {1}.",
|
||||
"closeFriendInvitationTitle": "Invitación de un amigo cercano",
|
||||
"closeFriendInvitationSubtitle": "Haga clic para ver la invitación",
|
||||
"closeFriendInvitationExpired": "La invitación para agregar un amigo cercano ha caducado. Esta acción no se puede realizar.",
|
||||
"closeFriendRequestDeclined": "Solicitud de amistad rechazada",
|
||||
"closeFriendsHaveBecome": "Se han hecho amigos cercanos",
|
||||
"otherAcceptedCloseFriendInvite": "La otra parte aceptó su invitación de amigo cercano.",
|
||||
"otherDeclinedCloseFriendInvite": "La otra parte rechazó su invitación de amigo cercano.",
|
||||
"youAcceptedCloseFriendInvite": "Aceptaste la invitación de amigo cercano de la otra parte.",
|
||||
"youDeclinedCloseFriendInvite": "Rechazaste la invitación de amigo cercano de la otra parte.",
|
||||
"closeFriendInviteTimedOut": "Se agotó el tiempo de espera para la invitación de amigo cercano.",
|
||||
"cpDismissConfirmMessage": "Eliminar esta relación costará {1} monedas. ¿Confirmar la eliminación?",
|
||||
"cpRelationCp": "CP",
|
||||
"cpRelationBrother": "Hermano",
|
||||
"cpRelationSister": "Hermana",
|
||||
"host": "Anfitrión",
|
||||
"following": "Siguiente",
|
||||
"agent": "Agencia",
|
||||
"approved": "Aprobado",
|
||||
"approvedWithinOneMinute": "Aprobado y mostrado dentro de 1 minuto después del envío",
|
||||
"onlineUsers": "Usuarios en línea ({1}/{2}):",
|
||||
"applyToJoin": "Aplicar para unirse",
|
||||
"supporterList": "Lista de seguidores",
|
||||
"hostList": "Lista de anfitriones",
|
||||
"upToAdmins": "Hasta {1} administradores",
|
||||
"upToMembers": "Hasta {1} miembros",
|
||||
"levelPrivileges": "Privilegios de nivel",
|
||||
"ra": "REAL ACADEMIA DE BELLAS ARTES",
|
||||
"roomAnnouncement": "Anuncio de la habitación",
|
||||
"help": "Ayuda",
|
||||
"rejected": "Rechazado",
|
||||
"boxContributeTips": "La inversión ya se ha realizado hoy. Por favor no vuelvas a invertir.",
|
||||
"bd": "BD",
|
||||
"coupon": "Cupón",
|
||||
"search": "Buscar",
|
||||
"get": "Conseguir",
|
||||
"inRocket": "en el cohete",
|
||||
"roomRocketHelpTips": "1. Enviar regalos en la habitación aumenta la energía del cohete. *1 moneda de oro de regalo = 1 punto de energía de cohete; Los obsequios de la suerte aumentan la energía del cohete en un 4% del valor de la moneda de oro del obsequio.\n2. Una vez que la energía del cohete esté completamente cargada, la sala puede lanzar el cohete. Las recompensas se distribuirán automáticamente después del lanzamiento.\n3. Los diferentes niveles de cohetes ofrecen diferentes recompensas.\n4. Cuando se lanza el cohete, todos los usuarios en la sala pueden reclamar la recompensa del cohete.\n5. La energía del cohete se reinicia a las 00:00 todos los días.",
|
||||
"couponRecord": "Registro de uso de cupones",
|
||||
"inRoom": "en la habitación",
|
||||
"searchCouponHint": "Buscar cupón",
|
||||
"giftCounter": "Mostrador de regalos",
|
||||
"bDLeaderInviteYouToBecomeBDLeader": "Te invitamos a convertirte en BDLeader",
|
||||
"wins": "gana",
|
||||
"inviteYouToBecomeHost": "Te invitamos a convertirte en anfitrión.",
|
||||
"friends": "Amigos",
|
||||
"deleteConversationTips": "¿Estás seguro de que deseas eliminar el historial de chat con este usuario?",
|
||||
"propMessagePrompt": "Mensaje de apoyo",
|
||||
"inputUserId": "Introduzca el ID de usuario",
|
||||
"fromLuckyGifts": "De regalos mágicos/afortunados",
|
||||
"receive": "Recibir",
|
||||
"checkInSuccessful": "Registro exitoso",
|
||||
"sginTips": "Recibirás una recompensa la primera vez que inicies sesión cada día. Si interrumpes tu racha de inicios de sesión, la recompensa se volverá a calcular desde el primer día cuando vuelvas a iniciar sesión.",
|
||||
"popular": "Popular",
|
||||
"recommend": "Recomendar",
|
||||
"follow": "Seguir",
|
||||
"history": "Historia",
|
||||
"hotRooms": "Habitaciones Calientes",
|
||||
"viewMore": "Ver más",
|
||||
"noData": "Sin datos",
|
||||
"users": "Usuarios",
|
||||
"rooms": "Alojamiento",
|
||||
"coins": "monedas",
|
||||
"unread": "No leído",
|
||||
"read": "Leer",
|
||||
"image": "[Imagen]",
|
||||
"video": "[Video]",
|
||||
"sound": "[Sonido]",
|
||||
"gift2": "[Regalo]",
|
||||
"clickHereToStartChatting": "Di algo.....",
|
||||
"receivedAMessage": "[Recibido un mensaje]",
|
||||
"confirmSwitchMicModelTips": "¿Confirmas el cambio en modo asiento?",
|
||||
"number": "Número",
|
||||
"album": "Álbum",
|
||||
"camera": "Cámara",
|
||||
"system": "Sistema",
|
||||
"notifcation": "Notificación",
|
||||
"inviteYouToBecomeAgent": "Te invitamos a convertirte en agencia.",
|
||||
"myMusic": "mi musica",
|
||||
"add": "Agregar",
|
||||
"pullToLoadMore": "Tira para cargar más",
|
||||
"loadingFailedClickToRetry": "Error al cargar, haga clic para volver a intentarlo",
|
||||
"releaseToLoadMore": "Suelta para cargar más",
|
||||
"haveMyLimits": "---Tengo mis límites.---",
|
||||
"music": "Música",
|
||||
"free": "Gratis",
|
||||
"charm": "Encanto de regalo",
|
||||
"start": "Comenzar",
|
||||
"stop": "Detener",
|
||||
"chats": "Charlas",
|
||||
"refuse": "Rechazar",
|
||||
"agree": "Aceptar",
|
||||
"thisFeatureIsCurrentlyUnavailable": "Esta característica no está disponible actualmente.",
|
||||
"pleaseSelectTheRecipient": "Por favor seleccione el destinatario.",
|
||||
"searchMemberIdHint": "Por favor ingrese el ID del miembro",
|
||||
"unclaimedRedEnvelopes": "Los sobres rojos no reclamados se reembolsan en 24 horas.",
|
||||
"redEnvelopeNotYetClaimed": "Sobre rojo aún no reclamado.",
|
||||
"redEnvelopeAmount": "Cantidad del sobre rojo: {1} monedas",
|
||||
"sentARedEnvelope": "Envió un sobre rojo.",
|
||||
"theRedEnvelopeHasExpired": "El sobre rojo ha caducado.",
|
||||
"joinRequest": "Solicitud de unión",
|
||||
"scrollToTheBottom": "Desplácese hasta el final",
|
||||
"gameRules": "Reglas del juego:",
|
||||
"charmGameRulesTips": "Encienda el panel para mostrar las monedas de regalo recibidas de todos los usuarios en el micrófono. 1 moneda = 1 puntuación (Regalo de la suerte: 1 moneda = 0,04 puntuación).",
|
||||
"inputYourOldPassword": "Ingrese su antigua contraseña",
|
||||
"enterYourOldPassword": "Ingrese su antigua contraseña",
|
||||
"setYourPassword": "Establece tu contraseña",
|
||||
"enterYourNewPassword": "Ingresa tu nueva contraseña",
|
||||
"confirmYourPassword": "Confirma tu contraseña",
|
||||
"theTwoPasswordsDoNotMatch": "Las dos contraseñas no coinciden.",
|
||||
"resetLoginPasswordtTips2": "La contraseña debe tener entre 8 y 16 caracteres y debe ser una combinación de letras inglesas mayúsculas y minúsculas y números (no solo números).",
|
||||
"resetLoginPassword": "Restablecer contraseña de inicio de sesión",
|
||||
"resetLoginPasswordtTips1": "Inicie sesión con su ID de usuario o ID personalizado. Es más seguro iniciar sesión con una cuenta yumi.",
|
||||
"localMusic": "Música Local",
|
||||
"setLoginPassword": "Establecer contraseña de inicio de sesión",
|
||||
"confirmSwitchMicThemeTips": "¿Confirmas el cambio de estilo de asiento?",
|
||||
"micTheme": "Tema de micrófono",
|
||||
"classicMic": "Micrófono clásico {1}",
|
||||
"yesterday": "Ayer {1}",
|
||||
"monday": "lunes {1}",
|
||||
"tuesday": "martes {1}",
|
||||
"wednesday": "miércoles {1}",
|
||||
"thursday": "jueves {1}",
|
||||
"friday": "viernes {1}",
|
||||
"saturday": "sábado {1}",
|
||||
"sunday": "domingo {1}",
|
||||
"acceptedYour": "{1} aceptó tu ",
|
||||
"youAccepted": "Aceptaste {1} ",
|
||||
"openRedPackDialogTip": "El sobre rojo",
|
||||
"micManagement": "Gestión de micrófonos",
|
||||
"bdCenter": "Centro BD",
|
||||
"rechargeAgency": "Agencia de recarga",
|
||||
"adminCenter": "Centro de administración",
|
||||
"managerCenter": "Centro de gerentes",
|
||||
"goldList": "Lista de oro",
|
||||
"followList": "Lista de seguimiento",
|
||||
"fansList": "Lista de fans",
|
||||
"daily": "A diario",
|
||||
"female": "Femenino",
|
||||
"male": "Masculino",
|
||||
"identity": "Identidad",
|
||||
"adjust": "Ajustar",
|
||||
"warning": "Advertencia",
|
||||
"screenshotTips": "Captura de pantalla (hasta 3)",
|
||||
"roomNotice": "Aviso de habitación",
|
||||
"roomTheme": "Tema de la habitación",
|
||||
"description": "Descripción:",
|
||||
"inputDesHint": "Describa el problema con el mayor detalle posible para que podamos comprenderlo y resolverlo.",
|
||||
"roomProfilePicture": "Foto de perfil de la habitación",
|
||||
"userProfilePicture": "Foto de perfil de usuario",
|
||||
"userName": "Nombre de usuario",
|
||||
"pleaseSelectTheTypeToProcess": "Por favor seleccione el tipo a procesar:",
|
||||
"roomEditing": "Edición de sala",
|
||||
"setAccount": "Establecer cuenta",
|
||||
"userEditing": "Edición de usuario",
|
||||
"enterTheUserId": "Introduzca el ID de usuario",
|
||||
"enterTheRoomId": "Ingrese el ID de la habitación",
|
||||
"deleteAccount": "Eliminar cuenta",
|
||||
"becomeAgent": "Conviértete en agente",
|
||||
"enterNickname": "Introduzca el apodo",
|
||||
"enterInvitationCode": "ingrese el código de invitación",
|
||||
"selectYourCountry": "Selecciona tu país",
|
||||
"inviteCode": "Código de invitación",
|
||||
"magic": "Magia",
|
||||
"luckGiftSpecialEffects": "Efectos de animación de regalo de la suerte.",
|
||||
"theVideoSizeCannotExceed": "El tamaño del vídeo no puede exceder los 50M.",
|
||||
"weekly": "Semanalmente",
|
||||
"customizedGiftRulesContent": "Cómo conseguir mi regalo personalizado:\n1.Determine si el usuario es elegible para recibir un obsequio personalizado según su nivel de riqueza actual.\n(1) Cuando el nivel de riqueza del usuario es ≥35: el usuario puede cargar un vídeo para el regalo personalizado una vez. Usaremos la imagen de perfil actual del usuario como imagen de regalo y el video proporcionado como efecto de regalo en \"Personalizado\". La producción llevará algún tiempo y te avisaremos tan pronto como esté disponible en la tienda.\n(2) Cuando el nivel de riqueza del usuario es ≥45: el usuario puede cargar un vídeo para el regalo personalizado una vez. Usaremos la imagen de perfil actual del usuario como imagen de regalo y el video proporcionado como efecto de regalo en \"Personalizado\". La producción llevará algún tiempo y te avisaremos tan pronto como esté disponible en la tienda.\n2.Puedes participar en actividades específicas en la aplicación y, luego de cumplir con los criterios, contactarnos a través de la sección \"Mensaje\" → \"Contáctanos\". Proporcione capturas de pantalla de la actividad y el vídeo que desea utilizar para el regalo personalizado. Usaremos su foto de perfil actual como imagen de regalo y el video proporcionado como efecto de regalo, luego lo incluiremos en \"Personalizado\". La producción llevará algún tiempo y te avisaremos tan pronto como esté disponible en la tienda.\nPeríodo de validez del regalo personalizado y cómo ampliarlo:\nLos obsequios personalizados exclusivos estarán disponibles en las estanterías durante un período de validez de 30 días. Para permitir que los usuarios influyentes e inspiradores sigan disfrutando de esta nueva experiencia y muestren su estilo personal, los usuarios que poseen obsequios personalizados pueden extender el tiempo de almacenamiento de todos sus obsequios personalizados en 30 días recargando $500 en cualquier mes determinado.",
|
||||
"customizedGiftRules": "Reglas de regalo personalizadas",
|
||||
"rulesUpload": "Reglas y carga",
|
||||
"monthly": "Mensual",
|
||||
"message": "Mensaje",
|
||||
"clearCache": "Borrar caché",
|
||||
"customized": "Personalizado",
|
||||
"searchInputHint": "Ingrese el número de cuenta/habitación",
|
||||
"kickRoomTips": "Te han echado de la habitación.",
|
||||
"roomKickedEntryBlockedTips": "Un administrador lo ha eliminado de la sala y no puede ingresar por ahora.",
|
||||
"joinRoomTips": "¡Se unió a la sala!",
|
||||
"roomSetting": "Configuración de la habitación",
|
||||
"roomDetails": "Detalles de la habitación",
|
||||
"systemRoomTips": "Sea cortés y respetuoso. Cualquier contenido pornográfico o vulgar está estrictamente prohibido en yumi. Una vez descubierta, la cuenta será prohibida permanentemente. Cumpla conscientemente con las regulaciones de la plataforma yumi.",
|
||||
"copiedToClipboard": "Copiado al portapapeles",
|
||||
"recharge": "Recargar",
|
||||
"receivedFromALuckyGift": "Recibido de un regalo mágico/afortunado.",
|
||||
"followedYou": "te seguí",
|
||||
"agentCenter": "Centro de agencia",
|
||||
"areYouSureYouWantToClearLocalCache": "¿Está seguro de que desea borrar el caché local?",
|
||||
"clearMessage": "Borrar mensajes en pantalla",
|
||||
"report": "Informe",
|
||||
"coins3": "monedas",
|
||||
"giftSpecialEffects": "efectos especiales de regalo",
|
||||
"basicFeatures": "Funciones básicas",
|
||||
"task": "Tarea",
|
||||
"importantReminder": "Recordatorio importante",
|
||||
"entryVehicleAnimation": "Animación del vehículo de entrada.",
|
||||
"floatingAnimationInGlobal": "Animación flotante en global.",
|
||||
"broadcast": "Transmisión",
|
||||
"broadcastDisplay": "Pantalla de transmisión",
|
||||
"broadcastAllPlaces": "Todos los lugares",
|
||||
"broadcastRoomOnly": "en la habitación",
|
||||
"broadcastOff": "Cerrar transmisión",
|
||||
"entryVehicleAnimation2": "Los usuarios con privilegios VIP4 o superiores pueden utilizar esta función para desactivar las animaciones del vehículo.",
|
||||
"dailyTasks": "Tareas Diarias",
|
||||
"exclusiveForNewcomers": "Exclusivo para recién llegados",
|
||||
"resetsDailyAtMidnight": "Se reinicia diariamente a medianoche",
|
||||
"limitedToOneTime": "Limitado a una vez",
|
||||
"useMicrophoneForOneMin": "Usa el micrófono durante 1 min.",
|
||||
"taskRequirement": "Requisito",
|
||||
"taskProgress": "Progreso",
|
||||
"rewardClaimedSuccessfully": "Recompensa reclamada exitosamente",
|
||||
"rewardClaimFailed": "Reclamación de recompensa fallida",
|
||||
"enterRoomConfirmTips": "¿Estás seguro de que quieres entrar a la habitación?",
|
||||
"followSucc": "Seguido con éxito",
|
||||
"goldListort": "Lista de oro",
|
||||
"rechargeList": "Lista de recarga",
|
||||
"edit": "Editar",
|
||||
"swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*Consejo: deslízate hacia la izquierda en el área de la pantalla flotante para cerrarla rápidamente.",
|
||||
"enterThisVoiceChatRoom": "¿Ingresar a esta sala de chat de voz?",
|
||||
"go": "Ir",
|
||||
"done": "Hecho",
|
||||
"improvementTasks": "Tareas de mejora",
|
||||
"save": "Ahorrar",
|
||||
"kickPrevention": "Prevención de patadas",
|
||||
"freeChatSpeak": "Chatear y hablar gratis",
|
||||
"nickName": "Apodo",
|
||||
"gender": "Género",
|
||||
"unFollow": "Dejar de seguir",
|
||||
"days": "Días",
|
||||
"permanent": "Permanente",
|
||||
"country": "País",
|
||||
"birthday": "Cumpleaños",
|
||||
"man": "Hombre",
|
||||
"woman": "Mujer",
|
||||
"apple": "Manzana",
|
||||
"google": "Google",
|
||||
"startVoiceParty": "¡Comienza una fiesta de voz!",
|
||||
"enterRoomTips": "{1} Entra en la habitación",
|
||||
"roomName": "Nombre de la habitación",
|
||||
"roomMember": "Miembro de la sala",
|
||||
"pleaseSelectYourCountry": "Por favor seleccione su país.",
|
||||
"pleaseSelectYourGender": "Por favor seleccione su género.",
|
||||
"pleaseEnterNickname": "Por favor ingrese un apodo.",
|
||||
"countryRegion": "País y región",
|
||||
"dateOfBirth": "Fecha de nacimiento",
|
||||
"mute": "Silenciar",
|
||||
"exit": "Salida",
|
||||
"mysteriousInvisibility": "Invisibilidad misteriosa",
|
||||
"antiBlock": "Antibloqueo",
|
||||
"privateChat": "Chat Privado",
|
||||
"storeDiscount": "Descuento en tienda {1} de descuento",
|
||||
"membershipFreeChatSpeak": "Chat & Speak sin membresía",
|
||||
"priorityRoomSorting": "Clasificación de habitaciones prioritarias",
|
||||
"userColoredID": "ID de color del usuario",
|
||||
"pleaseSelectaItem": "Por favor seleccione un artículo",
|
||||
"areYouRureRoRecharge": "¿Estás seguro de que quieres recargar?",
|
||||
"mInimize": "Mantener",
|
||||
"everyone": "Todos",
|
||||
"shop": "Comercio",
|
||||
"roomOwner": "Dueño de la habitación",
|
||||
"dailyTaskRewardBonus": "Bonificación de recompensa de tarea diaria ({1} XP)",
|
||||
"userLevelXPBoost": "Aumento de XP de nivel de usuario ({1} XP)",
|
||||
"andAboveUsers": "{1} y usuarios superiores",
|
||||
"privileges": "{1} Privilegios",
|
||||
"basicPermissions": "Permisos básicos",
|
||||
"hostCenter": "Centro anfitrión",
|
||||
"allOnMicrophone": "Todo en micrófono",
|
||||
"usersOnMicrophone": "Usuarios en el micrófono",
|
||||
"allInTheRoom": "todo en la habitación",
|
||||
"send": "Enviar",
|
||||
"crop": "Cultivo",
|
||||
"goToUpgrade": "Ir a actualizar",
|
||||
"exclusiveEmojiWillBeReleasedAfterBecoming": "Se lanzarán emoji exclusivos después de convertirse",
|
||||
"preventBeingBlocked": "Evitar ser bloqueado",
|
||||
"enableRankIncognitoMode": "Habilitar el modo de incógnito de clasificación",
|
||||
"avoidBeingKicked": "Evite ser pateado",
|
||||
"finish": "Finalizar",
|
||||
"takeTheMic": "Toma el micrófono",
|
||||
"openTheMic": "Abre el micrófono",
|
||||
"muteTheMic": "Silenciar el micrófono",
|
||||
"unlockTheMic": "Desbloquea el micrófono",
|
||||
"leavelTheMic": "dejar micrófono",
|
||||
"lockTheMic": "Bloquear el micrófono",
|
||||
"removeTheMic": "quitar el micrófono",
|
||||
"inviteToTheMicrophone": "Invitar al micrófono",
|
||||
"openUserProfleCard": "Abrir tarjeta de perfil de usuario",
|
||||
"obtain": "obtener",
|
||||
"win2": "Gana {1}",
|
||||
"backTheRoom": "cuarto trasero",
|
||||
"toConsume": "consumir",
|
||||
"howToUpgrade": "¿Cómo actualizar?",
|
||||
"spendCoinsToGainExperiencePoints": "Gasta monedas para ganar puntos de experiencia.",
|
||||
"richLevelUpgradeAnswer": "Deja que tu riqueza lo conquiste todo",
|
||||
"charmLevelUpgradeAnswer": "Haz que los demás se sometan a tu encanto",
|
||||
"higherLevelFancierAvatarFrame": "Insignias/marco de avatar de nivel superior y más elegantes",
|
||||
"all": "Todo",
|
||||
"gift": "Regalo",
|
||||
"chat": "Charlar",
|
||||
"owner": "Dueño",
|
||||
"store": "Almacenar",
|
||||
"admin": "Administración",
|
||||
"member": "Miembro",
|
||||
"guest": "Invitado",
|
||||
"submit": "Entregar",
|
||||
"claim": "Afirmar",
|
||||
"claimed": "Reclamado",
|
||||
"complete": "Completo",
|
||||
"shareTo": "Compartir con",
|
||||
"copyLink": "Copiar enlace",
|
||||
"faceBook": "Facebook",
|
||||
"whatsApp": "Whatsapp",
|
||||
"snapChat": "Snapchat",
|
||||
"taskNamePersonalGameConsume": "Gasto en juegos",
|
||||
"taskNameRoomNewMember": "Nuevos miembros de la sala",
|
||||
"taskNameRoomOwnerSendRedPacket": "El propietario de la habitación envía un paquete rojo",
|
||||
"taskNameRoomOwnerSendGiftUser": "El propietario de la habitación envía un regalo",
|
||||
"taskNameRoomMicUser120Min": "Miembros con más de 120 minutos en el micrófono",
|
||||
"taskNameRoomMicUser60Min": "Miembros con más de 60 minutos en el micrófono",
|
||||
"taskNameRoomMicUser30Min": "Miembros con más de 30 minutos en el micrófono",
|
||||
"taskNamePersonalSendGift": "Enviar regalo al usuario",
|
||||
"taskNameRoomOnlineUserCount": "Miembros de la sala en línea",
|
||||
"taskNameRoomOwnerInviteMic": "Invitar miembro al micrófono",
|
||||
"taskNameRoomUserSendGiftGold": "Monedas regaladas por los miembros",
|
||||
"taskNameRoomOwnerSendGiftGold": "Monedas regaladas por el dueño de la habitación",
|
||||
"taskNamePersonalMagicGiftGold": "Monedas ganadas enviando regalos mágicos",
|
||||
"taskNamePersonalLuckyGiftGold": "Monedas ganadas enviando regalos de la suerte",
|
||||
"taskNameRoomOwnerMicTime": "El propietario de la sala utiliza el micrófono en la sala",
|
||||
"taskNamePersonalActiveInRoom": "Sea activo en las habitaciones de los demás",
|
||||
"taskNamePersonalMicInRoom": "Ir al micrófono",
|
||||
"dailyCoinBonanzaRulesDetail": "1. Las tareas personales diarias y las tareas diarias del propietario de la sala se pueden completar una vez por usuario al día. Las tareas se reinician a las 00:00:00 hora de Arabia Saudita.\n2. Las tareas personales diarias sólo se pueden completar en las habitaciones de otras personas; Las tareas del propietario de la sala solo se pueden completar en su propia sala.\n3. Las tareas de entrega de regalos solo cuentan los regalos enviados en las salas, no los regalos enviados en los feeds.\n4. Si se crean varias cuentas usando el mismo dispositivo o la misma tarjeta SIM de alguna manera, las recompensas por todas las tareas solo se pueden reclamar una vez.",
|
||||
"dailyCoinBonanzaRules": "Reglas diarias de bonanza de monedas",
|
||||
"roomOwnerTasks": "Tareas del propietario de la sala",
|
||||
"personalTasks": "Tareas personales",
|
||||
"noPromptsToday": "No hay tareas hoy.",
|
||||
"getPaidToRefer": "Recibe dinero por recomendar",
|
||||
"membershipFee": "Cuota de membresía",
|
||||
"membershipFeeTips1": "Establezca la tarifa de membresía para su habitación. Los usuarios pueden unirse a su sala pagando la tarifa.",
|
||||
"membershipFeeTips2": "Los oros necesarios para que el usuario se convierta en miembro de la sala. El propietario de la sala obtendrá el 50% de los oros.",
|
||||
"freePrice": "Tarifa: 0-10000",
|
||||
"touristsSendText": "Turista envía texto",
|
||||
"touristsTakeToTheMic": "Los turistas toman el micrófono",
|
||||
"theMembershipFee": "La cuota de membresía",
|
||||
"theModificationsMade": "Las modificaciones realizadas esta vez no se guardarán después de salir.",
|
||||
"viewFrame": "Ver marco",
|
||||
"enterRoomName": "Introduce el nombre de la habitación",
|
||||
"headdress": "Marcos",
|
||||
"mountains": "Vehículos",
|
||||
"purchaseIsSuccessful": "La compra es exitosa",
|
||||
"buy": "Comprar",
|
||||
"followed": "Seguido",
|
||||
"follow2": "Seguir:{1}",
|
||||
"fans2": "Fanáticos:{1}",
|
||||
"vistors2": "Visitantes:{1}",
|
||||
"personal2": "Personal:",
|
||||
"conntinue": "Continuar",
|
||||
"confirmBuyTips": "¿Estás seguro de que quieres comprar?",
|
||||
"purchase": "Compra",
|
||||
"setRoomPassword": "Establecer contraseña de sala",
|
||||
"inputRoomPassword": "Introduzca la contraseña de la sala",
|
||||
"enter": "Ingresar",
|
||||
"deleteCommentTips": "¿Estás seguro de que quieres eliminar este comentario?",
|
||||
"deleteSuccessful": "Eliminación exitosa!",
|
||||
"itemsLeft": "Artículos restantes",
|
||||
"password": "Contraseña",
|
||||
"replySucc": "Responder exitosamente",
|
||||
"comment": "Comentario",
|
||||
"showMore": "Mostrar más",
|
||||
"showLess": "Mostrar menos",
|
||||
"enterPassword": "Ingrese la contraseña",
|
||||
"enterAccount": "Ingresar cuenta",
|
||||
"logIn": "Acceso",
|
||||
"saySomething": "Di algo...",
|
||||
"sayHi": "Di hola..",
|
||||
"pleaseChatFfriendly": "Por favor charla amablemente",
|
||||
"unLockTheRoom": "Desbloquear la habitación",
|
||||
"operationSuccessful": "La operación fue exitosa.",
|
||||
"adminByHomeowner": "es establecido como administrador por el propietario de la vivienda.",
|
||||
"memberByHomeowner": "es establecido como miembros por el propietario de la vivienda.",
|
||||
"touristByHomeowner": "es catalogado como turista por el propietario de la vivienda.",
|
||||
"becomeHost": "Solicite convertirse en anfitrión",
|
||||
"superFans": "Súper fanáticos:",
|
||||
"setUpAnIdentity": "Configurar una identidad",
|
||||
"kickedOutOfRoom": "expulsado de la habitación",
|
||||
"knapsack": "Mochila",
|
||||
"bdLeader": "Líder BD",
|
||||
"picture": "Imagen",
|
||||
"theImageSizeCannotExceed": "El tamaño de la imagen no puede exceder los 4M.",
|
||||
"activity": "Actividad",
|
||||
"alreadyAnAdministrator": "Ya soy administrador",
|
||||
"alreadyAnMember": "Ya eres miembro",
|
||||
"alreadyAnTourist": "ya un turista",
|
||||
"touristsCannotSendMessages": "Los turistas no pueden enviar mensajes.",
|
||||
"touristsAreNotAllowedToGoOnTheMic": "Los turistas no pueden usar el micrófono",
|
||||
"lockTheRoom": "cerrar la habitación",
|
||||
"special": "Especial",
|
||||
"visitorList": "Lista de visitantes",
|
||||
"successfulWear": "Desgaste exitoso",
|
||||
"confirmUnUseTips": "¿Confirmas eliminarlo?",
|
||||
"custom": "Costumbre",
|
||||
"customBackground": "Fondo personalizado",
|
||||
"example": "Ejemplo",
|
||||
"endPreview": "Vista previa final",
|
||||
"selectAgain": "Seleccionar de nuevo",
|
||||
"myItems": "Mis artículos",
|
||||
"use": "Usar",
|
||||
"unUse": "Desequipar",
|
||||
"renewal": "Renovación",
|
||||
"wallet": "Billetera",
|
||||
"profile": "Perfil",
|
||||
"giftwall": "Muro de regalos",
|
||||
"announcement": "Anuncio",
|
||||
"blockedList": "Lista de bloqueados",
|
||||
"country2": "País:",
|
||||
"sendTo": "Enviar a",
|
||||
"credits": "Créditos: {1}",
|
||||
"successfullyUnloaded": "descargado exitosamente",
|
||||
"expired": "Venció",
|
||||
"day": "Día",
|
||||
"inUse": "En uso",
|
||||
"staticOrGifImage": "Imagen estática / GIF",
|
||||
"confirmUseTips": "¿Quieres confirmar su uso?",
|
||||
"pleaseUploadUserAvatar": "Por favor sube un avatar.",
|
||||
"joinMemberTips": "Si es visitante en la sala, no podrá tomar el micrófono.",
|
||||
"giftGivingSuccessful": "Entrega de regalos exitosa.",
|
||||
"theAccountPasswordCannotBeEmpty": "La cuenta o contraseña no puede estar vacía.",
|
||||
"invitesYouToTheMicrophone": "{1} te invita al micrófono",
|
||||
"english": "Inglés",
|
||||
"chinese": "Chino",
|
||||
"arabic": "árabe",
|
||||
"darkMode": "Modo oscuro",
|
||||
"lightMode": "Modo de luz",
|
||||
"systemDefault": "Valor predeterminado del sistema",
|
||||
"pleaseGetOnTheMicFirst": "Por favor, sube al micrófono primero.",
|
||||
"roomMusicAddMusic": "Agregar música",
|
||||
"roomMusicSelectAll": "Seleccionar todo",
|
||||
"roomMusicAddedCount": "Se agregaron {1} canciones",
|
||||
"roomMusicEmpty": "sin musica",
|
||||
"roomMusicScanFromPhone": "Escanear desde el teléfono",
|
||||
"roomMusicTotalSongs": "Total: {1} canciones",
|
||||
"roomMusicNotFound": "No se encontró música",
|
||||
"roomMusicDeleteConfirm": "¿Eliminar esta música?",
|
||||
"roomMusicDeleted": "Música eliminada",
|
||||
"roomMusicListMode": "jugar en orden",
|
||||
"roomMusicShuffleMode": "Juego aleatorio",
|
||||
"roomMusicSingleMode": "Bucle único",
|
||||
"duration2": "Duración:{1}",
|
||||
"roomRocketNoteTitle": "Nota",
|
||||
"roomRocketNoteSectionTitle": "Cada vez que un VIP",
|
||||
"roomRocketNoteSectionBody": "1. Cada vez que se compra un nivel VIP, es válido por 30 días y se puede renovar manualmente una vez vencido.",
|
||||
"roomRocketRewardEmptyText": "Sin recompensa",
|
||||
"roomRocketRecordEmptyText": "Sin registros de recompensa",
|
||||
"launchRocket": "Lanzamiento de cohete",
|
||||
"roomRocketFuelFullNotice": "¡El combustible para cohetes está lleno! ¡Lanzamiento en {1} s!"
|
||||
}
|
||||
@ -1,750 +0,0 @@
|
||||
{
|
||||
"signInWithGoogle": "Masuk dengan Google",
|
||||
"or": "Atau",
|
||||
"signInWithYourAccount": "Masuk dengan akun Anda",
|
||||
"signInWithApple": "Masuk dengan Apple",
|
||||
"loginRepresentsAgreementTo": "Login mewakili persetujuan untuk",
|
||||
"termsofService": "Persyaratan layanan",
|
||||
"privaceyPolicy": "Kebijakan Privasi",
|
||||
"tips": "Kiat",
|
||||
"searchNoDataTips": "Masukkan ruangan atau ID pengguna yang ingin Anda cari.",
|
||||
"games": "Pertandingan",
|
||||
"mine": "Milikku",
|
||||
"official": "Resmi",
|
||||
"party": "Berpesta",
|
||||
"other": "Lainnya",
|
||||
"yes": "Ya",
|
||||
"bag": "Tas",
|
||||
"sound2": "Suara",
|
||||
"startYourBrandNewJourney": "Mulailah Perjalanan Baru Anda",
|
||||
"maliciousHarassment": "Pelecehan yang berbahaya",
|
||||
"event": "Peristiwa",
|
||||
"roomEdit": "Pengeditan Ruangan",
|
||||
"cancelRoomPassword": "Apakah Anda yakin ingin menghapus kata sandi ruangan?",
|
||||
"roomMemberFee": "Biaya Anggota Kamar",
|
||||
"roomTheme2": "Tema Kamar",
|
||||
"background": "Latar belakang",
|
||||
"blockedList2": "Daftar yang Diblokir",
|
||||
"roomPassword": "Kata Sandi Kamar",
|
||||
"numberOfMic": "Jumlah Mikrofon",
|
||||
"pleaseEnterContent": "Silakan masukkan konten",
|
||||
"profilePhoto": "Foto Profil",
|
||||
"aboutMe": "Tentang saya",
|
||||
"myRoom": "Kamarku",
|
||||
"noHistoricalRecordsAvailable": "Tidak ada catatan sejarah yang tersedia.",
|
||||
"inviteNewUsersToEarnCoins": "Undang pengguna baru untuk mendapatkan koin",
|
||||
"crateMyRoom": "BUAT RUANG ANDA SENDIRI.",
|
||||
"casualInteraction": "Interaksi Santai",
|
||||
"haveGamePlayingTips": "Anda memiliki permainan yang sedang berlangsung. Silakan keluar dari permainan saat ini. Apakah Anda yakin ingin keluar?",
|
||||
"historicalTour": "Wisata Sejarah",
|
||||
"gameCenter": "Pusat Permainan",
|
||||
"returnToVoiceChat": "Kembali ke obrolan suara?",
|
||||
"exitGameMode": "Keluar dari Mode Permainan",
|
||||
"enterTheRoom": "Masuki ruangan",
|
||||
"invite": "Mengundang",
|
||||
"recent": "Terkini",
|
||||
"popularEvents": "Acara Populer",
|
||||
"inviteGoRoomTips": "Selalu di sini untukmu, hujan atau cerah. Mampirlah dan sapa!",
|
||||
"confirmInviteThisUserToTheRoom": "Konfirmasikan undangan pengguna ini (ID:{1}) ke ruang?",
|
||||
"clearCacheSuccessfully": "Hapus cache berhasil",
|
||||
"sent": "Terkirim",
|
||||
"keep": "Menyimpan",
|
||||
"open": "Membuka",
|
||||
"deleteAccountTips2": "*Jika Anda berubah pikiran, Anda dapat masuk kembali ke akun Anda saat ini dalam waktu tujuh hari, dan kami akan memulihkan akun Anda secara otomatis. Jika tidak ada pemulihan yang terjadi dalam waktu tujuh hari, akun akan dihapus secara permanen.",
|
||||
"deleteAccountTips": "Anda memiliki hak administratif penuh atas akun ini. Jika Anda bermaksud menghapus akun, harap perhatikan risiko berikut yang terkait dengan operasi ini:\n1. Setelah akun berhasil dihapus, Anda tidak dapat lagi masuk ke akun Anda saat ini. Penghapusan akun merupakan tindakan permanen.\n2. Setelah akun berhasil dihapus, Anda tidak akan dapat memulihkan data akun apa pun. Semua informasi (termasuk kamar dan teman), mata uang virtual, hadiah, dan barang virtual akan dihapus secara permanen dan tidak dapat dipulihkan.\n3. Masa tunggu: Jika Anda tidak memulihkan akun, Anda tidak akan dapat mengakses halaman pembelian, halaman penarikan, atau halaman lainnya di aplikasi.\n4. Selama masa tunggu atau setelah akun dihapus, halaman profil akan menunjukkan bahwa akun tersebut telah dihapus. Untuk melindungi akun Anda agar tidak dicari atau diakses oleh orang lain, informasi pribadi Anda akan dihapus dari sistem yang terkait dengan fungsi sehari-hari. Jika penghapusan akun melibatkan keamanan nasional, proses perdata atau pidana, atau perlindungan hak dan kepentingan sah pihak ketiga, tim resmi berhak menolak permintaan penghapusan akun pengguna.\nJika Anda yakin ingin menghapus semua data pribadi dari akun Anda saat ini, silakan klik \"Hapus Akun\".",
|
||||
"accountDeletionNotice": "Pemberitahuan Penghapusan Akun:",
|
||||
"thisUserHasBeenBlacklisted": "Pengguna ini telah masuk daftar hitam.",
|
||||
"trend": "Kecenderungan",
|
||||
"like": "Menyukai",
|
||||
"more": "Lagi",
|
||||
"discard": "Membuang",
|
||||
"catchFirstComment": "Tangkap komentar pertama",
|
||||
"reply": "Membalas",
|
||||
"posting": "Posting",
|
||||
"multiple": "Banyak",
|
||||
"successfullyRemovedFromTheBlacklist": "Berhasil dihapus dari daftar hitam!",
|
||||
"successfullyAddedToTheBlacklist": "Berhasil ditambahkan ke daftar hitam!",
|
||||
"youAreCurrentlyCPRelationshipPleaseDissolve": "Anda sedang menjalin hubungan CP.\nSilakan dilarutkan terlebih dahulu.",
|
||||
"areYouSureToCancelBlacklist": "Apakah Anda yakin untuk membatalkan daftar hitam?",
|
||||
"areYouSureYouWantToBlockThisUser": "Apakah Anda yakin ingin memblokir pengguna ini?",
|
||||
"removeFromBlacklist": "Hapus dari daftar hitam",
|
||||
"moveToBlacklist": "Pindah ke daftar hitam",
|
||||
"userBlacklist": "Daftar hitam pengguna",
|
||||
"specialEffectsManagement": "Manajemen efek khusus",
|
||||
"wishingYouHappinessEveryDay": "Semoga Anda bahagia setiap hari.",
|
||||
"newMessage": "Pesan baru",
|
||||
"createRoomSuccsess": "Ruangan berhasil dibuat!",
|
||||
"contactUs": "Hubungi kami",
|
||||
"systemAnnouncementTips1": "Penjagaan terhadap penipuan:",
|
||||
"systemAnnouncementTips": "Verifikasi informasi hanya melalui saluran resmi. Jangan pernah mengunduh perangkat lunak pihak ketiga, membagikan data pribadi, atau mentransfer uang berdasarkan permintaan eksternal. ID staf resmi hanya 10000, 10003, dan 10086. Jika ada kecurigaan, hentikan dan laporkan melalui",
|
||||
"systemAnnouncement": "Pengumuman Sistem",
|
||||
"doNotClickUnfamiliarTips": "Jangan klik tautan yang tidak dikenal, karena dapat mengungkap informasi pribadi Anda. Jangan pernah membagikan detail ID atau kartu bank Anda kepada siapa pun.",
|
||||
"atTag": "@Menandai",
|
||||
"sayHi2": "Katakan Hai",
|
||||
"roomBottomGreeting": "Hai...",
|
||||
"canSendMsgTips": "Kedua belah pihak harus saling mengikuti sebelum mereka dapat mengirim pesan pribadi.",
|
||||
"msgSendRedEnvelopeTips": "*Biaya layanan sebesar 10% akan dikenakan pada amplop merah, dan penerima hanya akan menerima 90% dari nilai amplop merah. Tingkat kekayaan pengirim harus lebih tinggi dari Level 10.",
|
||||
"reapply": "Melamar lagi",
|
||||
"cancelRequest": "Batalkan Permintaan",
|
||||
"pending": "Tertunda",
|
||||
"supporter": "Pendukung",
|
||||
"coinsReceived": "Koin Diterima",
|
||||
"numberOfSign": "Jumlah tanda: {1}",
|
||||
"hostWeeklyRank": "Peringkat Mingguan Tuan Rumah",
|
||||
"supporterWeeklyRank": "Peringkat Mingguan Suporter",
|
||||
"memberList": "Daftar Anggota",
|
||||
"treasureChest": "Peti Harta Karun",
|
||||
"applicationRecord": "Catatan Aplikasi",
|
||||
"appUpdateTip": "Aplikasi ini memiliki versi baru ({1}). Apakah Anda ingin mengunduhnya sekarang?",
|
||||
"ownerIncomeCoins": "Pendapatan pemilik: {1} koin",
|
||||
"game": "Permainan",
|
||||
"skip2": "Melewati",
|
||||
"skipCountdown": "Lewati {1} detik",
|
||||
"coins4": "Koin",
|
||||
"weekStart": "Awal Minggu",
|
||||
"forMoreRewardsPleaseCheckTheTaskCenter": "Untuk hadiah lebih lanjut, silakan periksa pusat tugas",
|
||||
"kingQuuen": "Raja-Ratu",
|
||||
"ramadan": "Ramadan",
|
||||
"updateNow": "Perbarui Sekarang",
|
||||
"allGames": "Semua Permainan",
|
||||
"fishClass": "Kelas Ikan",
|
||||
"greedyClass": "Kelas serakah",
|
||||
"raceSeries": "Seri Balapan",
|
||||
"slotsClass": "Kelas Slot",
|
||||
"others": "Yang lain",
|
||||
"hotGames": "Permainan Panas",
|
||||
"chatBox": "Kotak obrolan",
|
||||
"termsOfServicePrivacyPolicyTips": "Dengan melanjutkan, Anda menyetujui Ketentuan Layanan & Kebijakan Privasi",
|
||||
"and": " Dan ",
|
||||
"pleaseSelectTheTypeContent": "Silakan pilih jenis konten yang menyinggung.",
|
||||
"illegalInformation": "Informasi ilegal",
|
||||
"inappropriateContent": "Konten yang tidak pantas",
|
||||
"personalAttack": "Serangan pribadi",
|
||||
"confirm": "Mengonfirmasi",
|
||||
"spam": "Spam",
|
||||
"countdownMinutes": "Menit Hitung Mundur:",
|
||||
"number2": "Nomor:",
|
||||
"fraud": "Tipuan",
|
||||
"received": "Diterima",
|
||||
"currentProgress": "Kemajuan saat ini",
|
||||
"currentStage": "Tahap saat ini:{1}",
|
||||
"roomReward2": "Hadiah Kamar:{1}",
|
||||
"roomReward": "Hadiah Kamar",
|
||||
"expirationTime": "Waktu kedaluwarsa",
|
||||
"ownerSendTheRedEnvelope": "Pemiliknya mengirimkan koin hadiah.",
|
||||
"rewardCoins": "Koin hadiah:{1} koin",
|
||||
"signInRewardReceived": "Berhasil masuk. Hadiah: {1}",
|
||||
"registerRewardReceived": "Hadiah pendaftaran diterima: {1}",
|
||||
"lastWeekProgress": "Kemajuan minggu lalu",
|
||||
"redEnvelopeTips2": "*Jika amplop merah tidak diklaim dalam batas waktu, sisa koin akan dikembalikan kepada pengguna yang mengirimkan amplop merah.",
|
||||
"goToRecharge": "Pergi untuk mengisi ulang",
|
||||
"deleteAccount2": " Hapus Akun ({1}s)",
|
||||
"areYouSureYouWantToDeleteYourAccount": " Apakah Anda yakin ingin menghapus akun Anda?",
|
||||
"insufhcientGoldsGoToRecharge": "Emas tidak mencukupi. Isi ulang sekarang!",
|
||||
"coins2": "{1} Koin",
|
||||
"remainingNumberTips": "Sisa tersedia: ({1}/{2})",
|
||||
"collectionTimeTips": "Waktu pengambilan:{1}({2}/{3})",
|
||||
"sendARedEnvelope": "Kirim amplop merah",
|
||||
"sendRedPackConfirmTips": "Apakah Anda yakin ingin mengirim paket merah?",
|
||||
"redEnvelopeSendingRecords": "Catatan pengiriman amplop merah:",
|
||||
"redEnvelope": "Amplop Merah",
|
||||
"roomRedPacketGoldCoins": "Koin Emas",
|
||||
"roomRedPacketQuantity": "Kuantitas",
|
||||
"roomRedPacketPickupTime": "Waktu Penjemputan",
|
||||
"roomRedPacketInMinutes": "Dalam {1} Menit",
|
||||
"roomRedPacketClaimNow": "Klaim Sekarang",
|
||||
"roomRedPacketSendLuckyPack": "Kirim Paket Keberuntungan",
|
||||
"roomRedPacketSending": "Mengirim...",
|
||||
"roomRedPacketSendSuccess": "Paket keberuntungan terkirim",
|
||||
"roomRedPacketComingSoon": "Segera hadir",
|
||||
"roomRedPacketRulesTitle": "Aturan",
|
||||
"roomRedPacketRulesContent": "1. Anda dapat menggunakan koin untuk mengirim Paket Keberuntungan. Jika orang lain membuka Paket Keberuntungan Anda, mereka dapat menerima sejumlah koin secara acak.\n\n2. Dua jam setelah Paket Keberuntungan dikirim, koin yang tidak diklaim akan dikembalikan ke dompet Anda.\n\n3. Setelah Anda mengirimkan Paket Keberuntungan di seluruh server, spanduk Paket Keberuntungan di seluruh server akan terpicu, sehingga memberi Anda lebih banyak perhatian.\n\n4. Anda dapat mengirimkan Paket Keberuntungan kamar sebesar %1$s per hari.\n\n5. Paket Keberuntungan yang dikirim oleh pengguna di luar wilayah Anda tidak dapat diklaim.",
|
||||
"roomRedPacketHistoryTitle": "Riwayat Pengiriman Paket Keberuntungan",
|
||||
"roomRedPacketHistoryItemTitle": "Tas Keberuntungan ({1} amplop merah)",
|
||||
"roomRedPacketHistoryStatusSent": "Terkirim",
|
||||
"roomRedPacketHistoryStatusFinished": "Diklaim sepenuhnya",
|
||||
"roomRedPacketHistoryStatusPending": "Tertunda",
|
||||
"roomRedPacketHistoryStatusReturned": "Kembali",
|
||||
"roomRedPacketHistoryRefund": "Pengembalian dana: {1}",
|
||||
"roomRedPacketClaimRecordsTitle": "Catatan Klaim",
|
||||
"roomRedPacketChatMessage": "Saya mengirimi Anda amplop merah, segera buka!",
|
||||
"roomRedPacketReceivedMessage": "Klaim {1} koin dari {2} Paket Keberuntungan",
|
||||
"roomRedPacketOpenSubtitle": "Paket Keberuntungan Telah Tiba",
|
||||
"roomRedPacketOpenYouGot": "Anda Dapatkan",
|
||||
"roomRedPacketClaimSuccessTips": "Koin telah dikirim ke dompet Anda.",
|
||||
"roomRedPacketOpenSoldOut": "Paket Keberuntungan Sudah Terjual Habis.",
|
||||
"roomRedPacketOpenExpired": "Paket Keberuntungan Telah Kedaluwarsa.",
|
||||
"roomRedPacketRegionNotMatch": "Hanya pengguna di wilayah bahasa yang sama yang dapat mengklaim paket keberuntungan ini.",
|
||||
"roomRedPacketNotStarted": "Paket keberuntungan belum siap.",
|
||||
"roomRedPacketDailyLimitExceeded": "Batas pengiriman hari ini telah tercapai.",
|
||||
"roomRedPacketInsufficientBalance": "Koin emas tidak mencukupi.",
|
||||
"roomRedPacketPresenceRequired": "Harap tetap di kamar untuk menggunakan paket keberuntungan.",
|
||||
"roomRedPacketNetworkError": "Tidak dapat memproses paket keberuntungan saat ini.",
|
||||
"roomRedPacketAlreadyClaimed": "Anda sudah mengklaim paket keberuntungan ini.",
|
||||
"roomRedPacketOpenWaiting": "Dapat dibuka dalam {1} menit.",
|
||||
"roomRedPacketOpenWaitingCountdown": "Tersedia Untuk Pengambilan Setelah {1}",
|
||||
"roomRedPacketOkay": "Oke",
|
||||
"roomRedPacketFloatingTitle": "{1} mengirimkan amplop merah",
|
||||
"roomRedPacketFloatingSubtitle": "Mengirim amplop merah",
|
||||
"redEnvelopeRecTips2": "Semua amplop merah telah diklaim.",
|
||||
"redEnvelopeRecTips3": "Waktu pengambilan amplop merah telah habis!",
|
||||
"openTheTreasureChest": "Buka peti harta karun",
|
||||
"redEnvelopeRecTips1": "Koin yang diperoleh telah disimpan ke dompet Anda.",
|
||||
"redEnvelopeTips1": "Koin:",
|
||||
"roomTools": "Peralatan Ruangan:",
|
||||
"entertainment": "Hiburan:",
|
||||
"reportSucc": "Laporkan berhasil",
|
||||
"pornography": "Pornografi",
|
||||
"reportInputTips": "Mohon jelaskan masalahnya sedetail mungkin agar kami dapat memahami dan menyelesaikannya.",
|
||||
"cancel": "Membatalkan",
|
||||
"join": "Bergabung",
|
||||
"items": "Barang",
|
||||
"vistors": "Pengunjung",
|
||||
"fans": "Penggemar",
|
||||
"balanceNotEnough": "Saldo koin emas tidak mencukupi. Apakah Anda ingin pergi ke top up?",
|
||||
"skip": "Lewati {1}",
|
||||
"letGoToWatch": "Ayo pergi menonton!",
|
||||
"launchedARocket": "meluncurkan roket",
|
||||
"sendUserId": "Kirim ID Pengguna:{1}",
|
||||
"giveUpIdentity": "Menyerahkan identitas",
|
||||
"leaveRoomIdentityTips": "Apakah Anda yakin ingin melepaskan identitas kamar?",
|
||||
"joinMemberTips2": "Apakah Anda ingin mengonfirmasi bergabung dengan ruang sebagai anggota?",
|
||||
"sureUnfollowThisRoom": "Yakin untuk berhenti mengikuti ruangan ini?",
|
||||
"welcomeMessage": "Selamat datang di aplikasi kami, {name}!",
|
||||
"settings": "Pengaturan",
|
||||
"account": "Akun",
|
||||
"common": "Umum",
|
||||
"delete": "Menghapus",
|
||||
"copy": "Menyalin",
|
||||
"bio": "Biografi",
|
||||
"introduction": "Perkenalan",
|
||||
"useCoupontips": "Apakah Anda yakin ingin menggunakan kupon tersebut?",
|
||||
"searchUserId": "Cari ID pengguna",
|
||||
"sendUser": "Kirim Pengguna",
|
||||
"hobby": "Hobi",
|
||||
"sendCoupontips": "Apakah Anda yakin ingin mengirimkan kupon ini kepada pengguna ini?",
|
||||
"youDontHaveAnyCouponsYet": "Anda belum memiliki kupon apa pun.",
|
||||
"recall": "Mengingat",
|
||||
"youHaventFollowed": "Anda belum mengikuti ruangan mana pun",
|
||||
"deleteFromMyDevice": "Hapus dari perangkat saya",
|
||||
"deleteOnAllDevices": "Hapus di semua perangkat",
|
||||
"messageHasBeenRecalled": "Pesan ini telah ditarik kembali",
|
||||
"recallThisMessage": "Ingat pesan ini?",
|
||||
"language": "Bahasa",
|
||||
"feedback": "Masukan",
|
||||
"signedin": "Masuk",
|
||||
"receiveSucc": "Berhasil diklaim",
|
||||
"about": "Tentang",
|
||||
"aboutUs": "Tentang Kami",
|
||||
"theme": "Tema",
|
||||
"wealthLevel": "Tingkat Kaya",
|
||||
"userLevel": "Tingkat Pesona",
|
||||
"goToUpload": "Pergi untuk mengunggah",
|
||||
"logout": "Keluar",
|
||||
"luck": "Keberuntungan",
|
||||
"level": "Tingkat",
|
||||
"themeGoToUploadTips": "1. Review akan selesai dalam waktu 24 jam setelah upload berhasil.\n2. Semua koin akan dikembalikan jika peninjauan gagal.",
|
||||
"pleaseUploadAccordingToExample": "Silakan upload sesuai contoh yang diberikan.",
|
||||
"home": "Rumah",
|
||||
"explore": "Mengeksplorasi",
|
||||
"me": "Aku",
|
||||
"socialPrivilege": "Hak istimewa sosial",
|
||||
"information": "Informasi",
|
||||
"myPhoto": "Foto saya",
|
||||
"areYouSureYouWantToSpend3": "*Jika pihak lain menolak undangan CP, koin Anda akan dikembalikan ke dompet Anda.",
|
||||
"areYouSureYouWantToSpend": "Apakah Anda yakin ingin membelanjakannya",
|
||||
"areYouSureYouWantToSpend2": "untuk mengirim undangan CP ke pengguna ini?",
|
||||
"underReview": "Sedang ditinjau",
|
||||
"doYouWantToDeleteIt": "Apakah Anda ingin menghapusnya?",
|
||||
"chooseFromAblum": "Pilih dari Album",
|
||||
"spaceBackground": "Latar Belakang Luar Angkasa",
|
||||
"editProfile": "Sunting Profil",
|
||||
"sendTheCpRequest": "Kirim permintaan CP",
|
||||
"addCp": "Tambahkan CP",
|
||||
"partWays": "Bagian Cara",
|
||||
"reconcile": "Mendamaikan",
|
||||
"separated": "Terpisah",
|
||||
"areYouSureYouWantToSpend5": "{1} menyatakan perasaannya padamu; jika kamu menerimanya, kamu akan menjadi pasangan.",
|
||||
"areYouSureYouWantToSpend6": "{1} ingin kembali bersama Anda. Jika Anda memutuskan untuk melakukan rekonsiliasi, semua data Anda sebelumnya akan dipulihkan.",
|
||||
"reconcileInvitationTips": "*Jika pihak lain menolak undangan CP, koin Anda akan dikembalikan ke dompet Anda.",
|
||||
"reconcileInvitation": "Rekonsiliasi Undangan",
|
||||
"areYouSureYouWantToSpend4": "mengirim undangan rekonsiliasi kepada pengguna ini?",
|
||||
"partWaysTips": "*Jika salah satu pasangan memilih untuk berpisah, akan ada periode jeda selama 7 hari. Selama jangka waktu tersebut, kedua belah pihak dapat memilih untuk melakukan rekonsiliasi, dan semua data akan dipulihkan setelah rekonsiliasi. Jika tidak ada rekonsiliasi yang dipilih pada akhir periode 7 hari, data pasangan tersebut akan dihapus.",
|
||||
"areYouSureYouWantToPartWaysWithYourCP": "Apakah Anda yakin ingin berpisah dengan CP Anda?",
|
||||
"timeSpentTogether": "Waktu yang dihabiskan bersama: {1} hari",
|
||||
"firstDay": "Hari pertama:{1}",
|
||||
"numberOfMyCPs": "Jumlah CP saya:({1}/{2})",
|
||||
"props": "Alat peraga",
|
||||
"win": "Menang",
|
||||
"dice": "Dadu",
|
||||
"rps": "RPS",
|
||||
"operationFail": "Operasi gagal.",
|
||||
"enterRoomFailedRetry": "Gagal masuk ruangan. Silakan coba lagi.",
|
||||
"voiceConnectionFailedRetry": "Sambungan suara gagal. Silakan coba lagi.",
|
||||
"likedYourComment": "Menyukai komentar Anda.",
|
||||
"doYouWantToKeepTheDraft": "Apakah Anda ingin menyimpan drafnya?",
|
||||
"operationsAreTooFrequent": "Operasi terlalu sering",
|
||||
"luckNumber": "Nomor Keberuntungan",
|
||||
"relationShip": "Hubungan",
|
||||
"couple": "Pasangan {1}:",
|
||||
"couple2": "Pasangan",
|
||||
"reject": "Menolak",
|
||||
"accept": "Menerima",
|
||||
"noMatchedCP": "Tidak ada CP yang cocok",
|
||||
"inviteYouToBecomeBD": "Mengundang Anda untuk menjadi BD.",
|
||||
"adminInviteRechargeAgent": "Mengundang Anda untuk menjadi agen Isi Ulang.",
|
||||
"confirmAcceptTheInvitation": "Konfirmasikan untuk menerima undangan?",
|
||||
"confirmDeclineTheInvitation": "Konfirmasi penolakan undangan?",
|
||||
"cpInviteRejectedByOther": "Pihak lain menolak menjadi {1}.",
|
||||
"closeFriendInvitationTitle": "Undangan dari Teman Dekat",
|
||||
"closeFriendInvitationSubtitle": "Klik untuk melihat undangan",
|
||||
"closeFriendInvitationExpired": "Undangan untuk menambah teman dekat telah kedaluwarsa. Tindakan ini tidak dapat dilakukan.",
|
||||
"closeFriendRequestDeclined": "Permintaan pertemanan ditolak",
|
||||
"closeFriendsHaveBecome": "Telah menjadi teman dekat",
|
||||
"otherAcceptedCloseFriendInvite": "Pihak lain menerima undangan teman dekat Anda.",
|
||||
"otherDeclinedCloseFriendInvite": "Pihak lain menolak undangan teman dekat Anda.",
|
||||
"youAcceptedCloseFriendInvite": "Anda menerima undangan teman dekat pihak lain.",
|
||||
"youDeclinedCloseFriendInvite": "Anda menolak undangan teman dekat pihak lain.",
|
||||
"closeFriendInviteTimedOut": "Batas waktu undangan teman dekat telah habis.",
|
||||
"cpDismissConfirmMessage": "Menghapus hubungan ini akan dikenakan biaya {1} koin. Konfirmasikan penghapusan?",
|
||||
"cpRelationCp": "CP",
|
||||
"cpRelationBrother": "Saudara laki-laki",
|
||||
"cpRelationSister": "Saudari",
|
||||
"host": "Tuan rumah",
|
||||
"following": "Mengikuti",
|
||||
"agent": "Agen",
|
||||
"approved": "Disetujui",
|
||||
"approvedWithinOneMinute": "Disetujui dan ditampilkan dalam waktu 1 menit setelah penyerahan",
|
||||
"onlineUsers": "Pengguna Daring ({1}/{2}):",
|
||||
"applyToJoin": "Ajukan permohonan untuk bergabung",
|
||||
"supporterList": "Daftar Pendukung",
|
||||
"hostList": "Daftar Tuan Rumah",
|
||||
"upToAdmins": "Hingga {1} Admin",
|
||||
"upToMembers": "Hingga {1} Anggota",
|
||||
"levelPrivileges": "Hak istimewa tingkat",
|
||||
"ra": "RA",
|
||||
"roomAnnouncement": "Pengumuman Kamar",
|
||||
"help": "Membantu",
|
||||
"rejected": "Ditolak",
|
||||
"boxContributeTips": "Investasi telah dilakukan hari ini. Tolong jangan berinvestasi lagi.",
|
||||
"bd": "BD",
|
||||
"coupon": "Kupon",
|
||||
"search": "Mencari",
|
||||
"get": "Mendapatkan",
|
||||
"inRocket": "Di dalam roket",
|
||||
"roomRocketHelpTips": "1. Mengirim hadiah di dalam ruangan meningkatkan energi roket. *1 hadiah koin emas = 1 titik energi roket; hadiah keberuntungan meningkatkan energi roket sebesar 4% dari nilai koin emas hadiah.\n2. Setelah energi roket terisi penuh, ruangan dapat meluncurkan roket. Hadiah akan didistribusikan secara otomatis setelah peluncuran.\n3. Level roket yang berbeda menawarkan hadiah yang berbeda.\n4. Saat roket diluncurkan, semua pengguna di dalam ruangan dapat mengklaim hadiah roket.\n5. Energi roket disetel ulang pada pukul 00:00 setiap hari.",
|
||||
"couponRecord": "Catatan penggunaan kupon",
|
||||
"inRoom": "Di Kamar",
|
||||
"searchCouponHint": "Cari Kupon",
|
||||
"giftCounter": "Penghitung hadiah",
|
||||
"bDLeaderInviteYouToBecomeBDLeader": "Mengundang Anda untuk menjadi BDLeader",
|
||||
"wins": "menang",
|
||||
"inviteYouToBecomeHost": "Mengundang Anda untuk menjadi tuan rumah.",
|
||||
"friends": "Teman-teman",
|
||||
"deleteConversationTips": "Apakah Anda yakin ingin menghapus riwayat obrolan dengan pengguna ini?",
|
||||
"propMessagePrompt": "Prompt pesan prop",
|
||||
"inputUserId": "Masukkan ID Pengguna",
|
||||
"fromLuckyGifts": "Dari hadiah keberuntungan/ajaib",
|
||||
"receive": "Menerima",
|
||||
"checkInSuccessful": "Check-in berhasil",
|
||||
"sginTips": "Anda akan mendapatkan hadiah saat pertama kali login setiap hari. Jika Anda menghentikan proses login berturut-turut, hadiah akan dihitung ulang dari hari pertama saat Anda login lagi.",
|
||||
"popular": "Populer",
|
||||
"recommend": "Menyarankan",
|
||||
"follow": "Mengikuti",
|
||||
"history": "Sejarah",
|
||||
"hotRooms": "Kamar Panas",
|
||||
"viewMore": "Lihat lebih banyak",
|
||||
"noData": "Tidak ada data",
|
||||
"users": "Pengguna",
|
||||
"rooms": "Kamar",
|
||||
"coins": "koin",
|
||||
"unread": "Belum dibaca",
|
||||
"read": "Membaca",
|
||||
"image": "[Gambar]",
|
||||
"video": "[Video]",
|
||||
"sound": "[Suara]",
|
||||
"gift2": "[Hadiah]",
|
||||
"clickHereToStartChatting": "Katakan sesuatu.....",
|
||||
"receivedAMessage": "[Menerima pesan]",
|
||||
"confirmSwitchMicModelTips": "Apakah Anda mengonfirmasi peralihan dalam mode tempat duduk?",
|
||||
"number": "Nomor",
|
||||
"album": "Album",
|
||||
"camera": "Kamera",
|
||||
"system": "Sistem",
|
||||
"notifcation": "Pemberitahuan",
|
||||
"inviteYouToBecomeAgent": "Mengundang Anda untuk menjadi agensi.",
|
||||
"myMusic": "Musik Saya",
|
||||
"add": "Menambahkan",
|
||||
"pullToLoadMore": "Tarik untuk memuat lebih banyak",
|
||||
"loadingFailedClickToRetry": "Gagal memuat, klik untuk mencoba lagi",
|
||||
"releaseToLoadMore": "Lepaskan untuk memuat lebih banyak",
|
||||
"haveMyLimits": "---Aku punya batasku.---",
|
||||
"music": "Musik",
|
||||
"free": "Bebas",
|
||||
"charm": "Pesona hadiah",
|
||||
"start": "Awal",
|
||||
"stop": "Berhenti",
|
||||
"chats": "Obrolan",
|
||||
"refuse": "Menolak",
|
||||
"agree": "Setuju",
|
||||
"thisFeatureIsCurrentlyUnavailable": "Fitur ini saat ini tidak tersedia.",
|
||||
"pleaseSelectTheRecipient": "Silakan pilih penerima.",
|
||||
"searchMemberIdHint": "Silakan masukkan ID anggota",
|
||||
"unclaimedRedEnvelopes": "Amplop merah yang tidak diambil akan dikembalikan dalam 24 jam.",
|
||||
"redEnvelopeNotYetClaimed": "Amplop merah belum diklaim.",
|
||||
"redEnvelopeAmount": "Jumlah amplop merah: {1} koin",
|
||||
"sentARedEnvelope": "mengirim amplop merah.",
|
||||
"theRedEnvelopeHasExpired": "Amplop merah sudah habis masa berlakunya.",
|
||||
"joinRequest": "Permintaan Gabung",
|
||||
"scrollToTheBottom": "Gulir ke bawah",
|
||||
"gameRules": "Aturan Permainan:",
|
||||
"charmGameRulesTips": "Aktifkan dasbor untuk menampilkan koin hadiah yang diterima semua pengguna di mikrofon. 1 koin = 1 skor (Hadiah keberuntungan: 1 koin = skor 0,04).",
|
||||
"inputYourOldPassword": "Masukkan kata sandi lama Anda",
|
||||
"enterYourOldPassword": "Masukkan kata sandi lama Anda",
|
||||
"setYourPassword": "Tetapkan kata sandi Anda",
|
||||
"enterYourNewPassword": "Masukkan kata sandi baru Anda",
|
||||
"confirmYourPassword": "Konfirmasikan kata sandi Anda",
|
||||
"theTwoPasswordsDoNotMatch": "Kedua kata sandi tidak cocok.",
|
||||
"resetLoginPasswordtTips2": "Kata sandi harus terdiri dari 8-16 karakter dan harus berupa kombinasi huruf besar dan kecil bahasa Inggris serta angka (bukan hanya angka).",
|
||||
"resetLoginPassword": "Atur Ulang Kata Sandi Masuk",
|
||||
"resetLoginPasswordtTips1": "Masuk dengan ID pengguna atau ID cantik Anda. Lebih aman masuk dengan akun yumi.",
|
||||
"localMusic": "Musik Lokal",
|
||||
"setLoginPassword": "Tetapkan Kata Sandi Masuk",
|
||||
"confirmSwitchMicThemeTips": "Apakah Anda mengonfirmasi peralihan gaya kursi?",
|
||||
"micTheme": "Tema Mikrofon",
|
||||
"classicMic": "Klasik {1} Mikrofon",
|
||||
"yesterday": "Kemarin {1}",
|
||||
"monday": "Senin {1}",
|
||||
"tuesday": "Selasa {1}",
|
||||
"wednesday": "Rabu {1}",
|
||||
"thursday": "Kamis {1}",
|
||||
"friday": "Jumat {1}",
|
||||
"saturday": "Sabtu {1}",
|
||||
"sunday": "Minggu {1}",
|
||||
"acceptedYour": "{1} menerima Anda ",
|
||||
"youAccepted": "Anda menyetujui {1} ",
|
||||
"openRedPackDialogTip": "Amplop merah",
|
||||
"micManagement": "Manajemen Mikrofon",
|
||||
"bdCenter": "Pusat BD",
|
||||
"rechargeAgency": "Badan Isi Ulang",
|
||||
"adminCenter": "Pusat Admin",
|
||||
"managerCenter": "Pusat Manajer",
|
||||
"goldList": "Daftar Emas",
|
||||
"followList": "Ikuti Daftar",
|
||||
"fansList": "Daftar Penggemar",
|
||||
"daily": "Sehari-hari",
|
||||
"female": "Perempuan",
|
||||
"male": "Pria",
|
||||
"identity": "Identitas",
|
||||
"adjust": "Menyesuaikan",
|
||||
"warning": "Peringatan",
|
||||
"screenshotTips": "Tangkapan Layar (Hingga 3)",
|
||||
"roomNotice": "Pemberitahuan kamar",
|
||||
"roomTheme": "Tema kamar",
|
||||
"description": "Keterangan:",
|
||||
"inputDesHint": "Mohon jelaskan masalahnya sedetail mungkin agar kami dapat memahami dan menyelesaikannya.",
|
||||
"roomProfilePicture": "Gambar profil kamar",
|
||||
"userProfilePicture": "Gambar profil pengguna",
|
||||
"userName": "Nama belakang",
|
||||
"pleaseSelectTheTypeToProcess": "Silakan pilih jenis yang akan diproses:",
|
||||
"roomEditing": "Pengeditan Ruangan",
|
||||
"setAccount": "Tetapkan Akun",
|
||||
"userEditing": "Pengeditan Pengguna",
|
||||
"enterTheUserId": "Masukkan ID pengguna",
|
||||
"enterTheRoomId": "Masukkan ID ruangan",
|
||||
"deleteAccount": "Hapus Akun",
|
||||
"becomeAgent": "Menjadi agen",
|
||||
"enterNickname": "Masukkan Nama Panggilan",
|
||||
"enterInvitationCode": "masukkan kode undangan",
|
||||
"selectYourCountry": "Pilih negara Anda",
|
||||
"inviteCode": "Kode Undangan",
|
||||
"magic": "Sihir",
|
||||
"luckGiftSpecialEffects": "Efek animasi hadiah keberuntungan",
|
||||
"theVideoSizeCannotExceed": "Ukuran video tidak boleh melebihi 50M",
|
||||
"weekly": "Mingguan",
|
||||
"customizedGiftRulesContent": "Cara mendapatkan hadiah khusus saya:\n1.Tentukan apakah pengguna berhak menerima hadiah khusus berdasarkan tingkat kekayaan mereka saat ini.\n(1) Ketika tingkat kekayaan pengguna ≥35: Pengguna dapat mengunggah video untuk hadiah khusus satu kali. Kami akan menggunakan gambar profil pengguna saat ini sebagai gambar hadiah dan video yang disediakan sebagai efek hadiah pada \"Disesuaikan\". Produksi akan memakan waktu beberapa saat, dan kami akan memberi tahu Anda segera setelah tersedia di toko.\n(2) Ketika tingkat kekayaan pengguna ≥45: Pengguna dapat mengunggah video untuk hadiah khusus satu kali. Kami akan menggunakan gambar profil pengguna saat ini sebagai gambar hadiah dan video yang disediakan sebagai efek hadiah pada \"Disesuaikan\". Produksi akan memakan waktu beberapa saat, dan kami akan memberi tahu Anda segera setelah tersedia di toko.\n2.Anda dapat berpartisipasi dalam aktivitas tertentu di aplikasi dan, setelah memenuhi kriteria, menghubungi kami melalui bagian \"Pesan\" → \"Hubungi kami\". Berikan tangkapan layar aktivitas dan video yang ingin Anda gunakan untuk hadiah khusus. Kami akan menggunakan gambar profil Anda saat ini sebagai gambar hadiah dan video yang disediakan sebagai efek hadiah, lalu mencantumkannya di bawah \"Disesuaikan\". Produksi akan memakan waktu beberapa saat, dan kami akan memberi tahu Anda segera setelah tersedia di toko.\nPeriode Validitas Hadiah yang Disesuaikan & Cara Memperpanjangnya:\nHadiah eksklusif yang disesuaikan akan tersedia di rak untuk masa berlaku 30 hari. Untuk memungkinkan pengguna yang berpengaruh dan menginspirasi untuk terus menikmati pengalaman baru ini dan menampilkan gaya pribadi mereka, pengguna yang memiliki hadiah khusus dapat memperpanjang waktu penyimpanan semua hadiah khusus mereka selama 30 hari dengan mengisi ulang $500 pada bulan tertentu.",
|
||||
"customizedGiftRules": "Aturan Hadiah yang Disesuaikan",
|
||||
"rulesUpload": "Aturan & Unggah",
|
||||
"monthly": "Bulanan",
|
||||
"message": "Pesan",
|
||||
"clearCache": "Hapus Tembolok",
|
||||
"customized": "Disesuaikan",
|
||||
"searchInputHint": "Masukkan nomor rekening/kamar",
|
||||
"kickRoomTips": "Anda telah diusir dari ruangan.",
|
||||
"roomKickedEntryBlockedTips": "Anda telah dikeluarkan dari ruangan oleh admin dan tidak dapat masuk untuk saat ini.",
|
||||
"joinRoomTips": "Bergabung dengan ruangan!",
|
||||
"roomSetting": "Pengaturan Ruangan",
|
||||
"roomDetails": "Detail Kamar",
|
||||
"systemRoomTips": "Bersikaplah sopan dan hormat. Konten pornografi atau vulgar apa pun dilarang keras di yumi. Setelah ditemukan, akun tersebut akan diblokir secara permanen. Harap secara sadar mematuhi peraturan platform yumi.",
|
||||
"copiedToClipboard": "Disalin ke papan klip",
|
||||
"recharge": "Isi ulang",
|
||||
"receivedFromALuckyGift": "Diterima dari hadiah keberuntungan/ajaib.",
|
||||
"followedYou": "Mengikutimu",
|
||||
"agentCenter": "Pusat Agensi",
|
||||
"areYouSureYouWantToClearLocalCache": "Apakah Anda yakin ingin menghapus cache lokal?",
|
||||
"clearMessage": "Hapus pesan layar",
|
||||
"report": "Laporan",
|
||||
"coins3": "Koin",
|
||||
"giftSpecialEffects": "Hadiahkan efek khusus",
|
||||
"basicFeatures": "Fitur Dasar",
|
||||
"task": "Tugas",
|
||||
"importantReminder": "Pengingat Penting",
|
||||
"entryVehicleAnimation": "Animasi kendaraan masuk",
|
||||
"floatingAnimationInGlobal": "Animasi mengambang secara global",
|
||||
"broadcast": "Siaran",
|
||||
"broadcastDisplay": "Tampilan siaran",
|
||||
"broadcastAllPlaces": "Semua tempat",
|
||||
"broadcastRoomOnly": "Di kamar",
|
||||
"broadcastOff": "Tutup siaran",
|
||||
"entryVehicleAnimation2": "Pengguna dengan hak istimewa VIP4 atau lebih tinggi dapat menggunakan fungsi ini untuk menonaktifkan animasi kendaraan.",
|
||||
"dailyTasks": "Tugas Harian",
|
||||
"exclusiveForNewcomers": "Eksklusif Untuk Pendatang Baru",
|
||||
"resetsDailyAtMidnight": "Direset setiap hari pada tengah malam",
|
||||
"limitedToOneTime": "Terbatas satu kali saja",
|
||||
"useMicrophoneForOneMin": "Gunakan mikrofon selama 1 menit",
|
||||
"taskRequirement": "Persyaratan",
|
||||
"taskProgress": "Kemajuan",
|
||||
"rewardClaimedSuccessfully": "Hadiah berhasil diklaim",
|
||||
"rewardClaimFailed": "Klaim hadiah gagal",
|
||||
"enterRoomConfirmTips": "Apakah Anda yakin ingin memasuki ruangan?",
|
||||
"followSucc": "Diikuti dengan sukses",
|
||||
"goldListort": "Daftar Emas",
|
||||
"rechargeList": "Daftar Isi Ulang",
|
||||
"edit": "Sunting",
|
||||
"swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt": "*Tips: Gesek ke kiri pada area layar mengambang untuk menutupnya dengan cepat.",
|
||||
"enterThisVoiceChatRoom": "Masuk ke ruang obrolan suara ini?",
|
||||
"go": "Pergi",
|
||||
"done": "Selesai",
|
||||
"improvementTasks": "Tugas Peningkatan",
|
||||
"save": "Menyimpan",
|
||||
"kickPrevention": "Pencegahan tendangan",
|
||||
"freeChatSpeak": "Obrolan & Bicara Gratis",
|
||||
"nickName": "Nama panggilan",
|
||||
"gender": "Jenis kelamin",
|
||||
"unFollow": "Berhenti mengikuti",
|
||||
"days": "hari",
|
||||
"permanent": "Permanen",
|
||||
"country": "Negara",
|
||||
"birthday": "Hari ulang tahun",
|
||||
"man": "Pria",
|
||||
"woman": "Wanita",
|
||||
"apple": "Apel",
|
||||
"google": "Google",
|
||||
"startVoiceParty": "Mulai pesta suara!",
|
||||
"enterRoomTips": "{1} Masuk ke ruangan",
|
||||
"roomName": "Nama Kamar",
|
||||
"roomMember": "Anggota Kamar",
|
||||
"pleaseSelectYourCountry": "Silakan pilih negara Anda.",
|
||||
"pleaseSelectYourGender": "Silakan pilih jenis kelamin Anda.",
|
||||
"pleaseEnterNickname": "Silakan masukkan nama panggilan.",
|
||||
"countryRegion": "Negara & Wilayah",
|
||||
"dateOfBirth": "Tanggal lahir",
|
||||
"mute": "Bisu",
|
||||
"exit": "KELUAR",
|
||||
"mysteriousInvisibility": "Gaib yang misterius",
|
||||
"antiBlock": "Anti-Blok",
|
||||
"privateChat": "Obrolan Pribadi",
|
||||
"storeDiscount": "Diskon Toko Diskon {1}",
|
||||
"membershipFreeChatSpeak": "Obrolan & Bicara bebas keanggotaan",
|
||||
"priorityRoomSorting": "Penyortiran Kamar Prioritas",
|
||||
"userColoredID": "ID Berwarna Pengguna",
|
||||
"pleaseSelectaItem": "Silakan pilih item",
|
||||
"areYouRureRoRecharge": "Apakah Anda yakin ingin mengisi ulang?",
|
||||
"mInimize": "Menyimpan",
|
||||
"everyone": "Setiap orang",
|
||||
"shop": "Toko",
|
||||
"roomOwner": "Pemilik Kamar",
|
||||
"dailyTaskRewardBonus": "Bonus Hadiah Tugas Harian ({1} XP)",
|
||||
"userLevelXPBoost": "Peningkatan XP Tingkat Pengguna ({1} XP)",
|
||||
"andAboveUsers": "{1} Dan Pengguna Di Atas",
|
||||
"privileges": "{1} Hak istimewa",
|
||||
"basicPermissions": "Izin dasar",
|
||||
"hostCenter": "Pusat Tuan Rumah",
|
||||
"allOnMicrophone": "Semuanya menggunakan mikrofon",
|
||||
"usersOnMicrophone": "Pengguna di mikrofon",
|
||||
"allInTheRoom": "Semua di kamar",
|
||||
"send": "Mengirim",
|
||||
"crop": "Tanaman",
|
||||
"goToUpgrade": "Pergi untuk meningkatkan",
|
||||
"exclusiveEmojiWillBeReleasedAfterBecoming": "Emoji eksklusif akan dirilis setelah menjadi",
|
||||
"preventBeingBlocked": "Mencegah Diblokir",
|
||||
"enableRankIncognitoMode": "Aktifkan Mode Penyamaran Peringkat",
|
||||
"avoidBeingKicked": "Hindari Ditendang",
|
||||
"finish": "Menyelesaikan",
|
||||
"takeTheMic": "Ambil mikrofonnya",
|
||||
"openTheMic": "Buka mikrofon",
|
||||
"muteTheMic": "Matikan mikrofon",
|
||||
"unlockTheMic": "Buka kunci mikrofon",
|
||||
"leavelTheMic": "Tinggalkan mikrofon",
|
||||
"lockTheMic": "Kunci mikrofon",
|
||||
"removeTheMic": "Hapus mikrofon",
|
||||
"inviteToTheMicrophone": "Undang ke mikrofon",
|
||||
"openUserProfleCard": "Buka kartu profil pengguna",
|
||||
"obtain": "memperoleh",
|
||||
"win2": "Menangkan {1}",
|
||||
"backTheRoom": "Ruang belakang",
|
||||
"toConsume": "Untuk mengkonsumsi",
|
||||
"howToUpgrade": "Bagaimana cara meningkatkannya?",
|
||||
"spendCoinsToGainExperiencePoints": "Habiskan Koin untuk mendapatkan poin pengalaman",
|
||||
"richLevelUpgradeAnswer": "Biarkan kekayaan Anda mengalahkan segalanya",
|
||||
"charmLevelUpgradeAnswer": "Buat orang lain tunduk pada pesona Anda",
|
||||
"higherLevelFancierAvatarFrame": "Tingkat yang lebih tinggi, lencana/bingkai avatar yang lebih menarik",
|
||||
"all": "Semua",
|
||||
"gift": "Hadiah",
|
||||
"chat": "Mengobrol",
|
||||
"owner": "Pemilik",
|
||||
"store": "Toko",
|
||||
"admin": "Admin",
|
||||
"member": "Anggota",
|
||||
"guest": "Tamu",
|
||||
"submit": "Kirim",
|
||||
"claim": "Mengeklaim",
|
||||
"claimed": "Diklaim",
|
||||
"complete": "Menyelesaikan",
|
||||
"shareTo": "Bagikan ke",
|
||||
"copyLink": "Salin Tautan",
|
||||
"faceBook": "Facebook",
|
||||
"whatsApp": "ada apa",
|
||||
"snapChat": "Snapchat",
|
||||
"taskNamePersonalGameConsume": "Pengeluaran Game",
|
||||
"taskNameRoomNewMember": "Anggota Ruang Baru",
|
||||
"taskNameRoomOwnerSendRedPacket": "Pemilik Kamar mengirimkan satu paket merah",
|
||||
"taskNameRoomOwnerSendGiftUser": "Pemilik Kamar Mengirim Hadiah",
|
||||
"taskNameRoomMicUser120Min": "Anggota dengan 120+ menit menggunakan mikrofon",
|
||||
"taskNameRoomMicUser60Min": "Anggota dengan 60+ menit menggunakan mikrofon",
|
||||
"taskNameRoomMicUser30Min": "Anggota dengan 30+ menit menggunakan mikrofon",
|
||||
"taskNamePersonalSendGift": "Kirim Hadiah ke Pengguna",
|
||||
"taskNameRoomOnlineUserCount": "Anggota ruang online",
|
||||
"taskNameRoomOwnerInviteMic": "Undang anggota ke Mic",
|
||||
"taskNameRoomUserSendGiftGold": "Koin yang diberikan oleh anggota",
|
||||
"taskNameRoomOwnerSendGiftGold": "Koin yang diberikan oleh pemilik kamar",
|
||||
"taskNamePersonalMagicGiftGold": "Koin Dimenangkan dengan Mengirim Hadiah Ajaib",
|
||||
"taskNamePersonalLuckyGiftGold": "Koin Dimenangkan dengan Mengirim Hadiah Keberuntungan",
|
||||
"taskNameRoomOwnerMicTime": "Pemilik Kamar menyalakan mikrofon di dalam kamar",
|
||||
"taskNamePersonalActiveInRoom": "Jadilah Aktif di Ruangan Orang Lain",
|
||||
"taskNamePersonalMicInRoom": "Ayo Mikrofon",
|
||||
"dailyCoinBonanzaRulesDetail": "1. Tugas Pribadi Harian dan Tugas Pemilik Ruangan Harian dapat diselesaikan satu kali per pengguna per hari. Tugas diatur ulang pada 00:00:00 waktu Saudi.\n2. Tugas pribadi sehari-hari hanya dapat diselesaikan di ruangan orang lain; tugas pemilik ruangan hanya dapat diselesaikan di ruangan Anda sendiri.\n3. Tugas pemberian hadiah hanya menghitung hadiah yang dikirim di ruangan, bukan hadiah yang dikirim di feed.\n4. Jika beberapa akun dibuat menggunakan perangkat yang sama atau kartu SIM yang sama dengan cara apa pun, hadiah untuk semua tugas hanya dapat diklaim satu kali.",
|
||||
"dailyCoinBonanzaRules": "Aturan Bonanza Koin Harian",
|
||||
"roomOwnerTasks": "Tugas Pemilik Ruangan",
|
||||
"personalTasks": "Tugas Pribadi",
|
||||
"noPromptsToday": "Tidak ada tugas hari ini.",
|
||||
"getPaidToRefer": "Dapatkan Bayaran untuk Merujuk",
|
||||
"membershipFee": "Biaya Keanggotaan",
|
||||
"membershipFeeTips1": "Silakan tentukan biaya keanggotaan untuk kamar Anda. Pengguna dapat bergabung dengan kamar Anda dengan membayar biayanya.",
|
||||
"membershipFeeTips2": "Emas yang dibutuhkan pengguna untuk menjadi anggota ruangan. Pemilik ruangan akan mendapatkan 50% emas.",
|
||||
"freePrice": "Biaya: 0-10.000",
|
||||
"touristsSendText": "Turis mengirim SMS",
|
||||
"touristsTakeToTheMic": "Wisatawan mendengarkan mikrofon",
|
||||
"theMembershipFee": "Biaya keanggotaan",
|
||||
"theModificationsMade": "Modifikasi yang dilakukan kali ini tidak akan disimpan setelah keluar",
|
||||
"viewFrame": "Lihat Bingkai",
|
||||
"enterRoomName": "Masukkan nama ruangan",
|
||||
"headdress": "Bingkai",
|
||||
"mountains": "Kendaraan",
|
||||
"purchaseIsSuccessful": "Pembelian berhasil",
|
||||
"buy": "Membeli",
|
||||
"followed": "Diikuti",
|
||||
"follow2": "Ikuti:{1}",
|
||||
"fans2": "Penggemar:{1}",
|
||||
"vistors2": "Pengunjung:{1}",
|
||||
"personal2": "Pribadi:",
|
||||
"conntinue": "Melanjutkan",
|
||||
"confirmBuyTips": "Apakah Anda yakin ingin membeli?",
|
||||
"purchase": "Pembelian",
|
||||
"setRoomPassword": "Tetapkan kata sandi ruangan",
|
||||
"inputRoomPassword": "Masukkan kata sandi ruangan",
|
||||
"enter": "Memasuki",
|
||||
"deleteCommentTips": "Apakah Anda yakin ingin menghapus komentar ini?",
|
||||
"deleteSuccessful": "Penghapusan berhasil!",
|
||||
"itemsLeft": "Barang tersisa",
|
||||
"password": "Kata sandi",
|
||||
"replySucc": "Balasan berhasil",
|
||||
"comment": "Komentar",
|
||||
"showMore": "Tampilkan lebih banyak",
|
||||
"showLess": "Tampilkan lebih sedikit",
|
||||
"enterPassword": "Masukkan Kata Sandi",
|
||||
"enterAccount": "Masuk ke Akun",
|
||||
"logIn": "Masuk",
|
||||
"saySomething": "Katakan sesuatu...",
|
||||
"sayHi": "Ucapkan salam..",
|
||||
"pleaseChatFfriendly": "Silakan ngobrol dengan ramah",
|
||||
"unLockTheRoom": "Buka Kunci Ruangan",
|
||||
"operationSuccessful": "Operasinya berhasil.",
|
||||
"adminByHomeowner": "ditetapkan sebagai administrator oleh pemilik rumah.",
|
||||
"memberByHomeowner": "ditetapkan sebagai anggota oleh pemilik rumah.",
|
||||
"touristByHomeowner": "ditetapkan sebagai turis oleh pemilik rumah.",
|
||||
"becomeHost": "Lamar Untuk Menjadi Tuan Rumah",
|
||||
"superFans": "Penggemar super:",
|
||||
"setUpAnIdentity": "Siapkan identitas",
|
||||
"kickedOutOfRoom": "Ditendang keluar dari ruangan",
|
||||
"knapsack": "ransel",
|
||||
"bdLeader": "Pemimpin BD",
|
||||
"picture": "Gambar",
|
||||
"theImageSizeCannotExceed": "Ukuran gambar tidak boleh melebihi 4M",
|
||||
"activity": "Aktivitas",
|
||||
"alreadyAnAdministrator": "Sudah menjadi administrator",
|
||||
"alreadyAnMember": "Sudah menjadi anggota",
|
||||
"alreadyAnTourist": "Sudah menjadi turis",
|
||||
"touristsCannotSendMessages": "Wisatawan tidak dapat mengirim pesan",
|
||||
"touristsAreNotAllowedToGoOnTheMic": "Wisatawan tidak diperbolehkan menggunakan mikrofon",
|
||||
"lockTheRoom": "Kunci Ruangan",
|
||||
"special": "Spesial",
|
||||
"visitorList": "Daftar Pengunjung",
|
||||
"successfulWear": "Pakaian yang sukses",
|
||||
"confirmUnUseTips": "Apakah Anda mengonfirmasi untuk menghapusnya?",
|
||||
"custom": "Kebiasaan",
|
||||
"customBackground": "Latar Belakang Kustom",
|
||||
"example": "Contoh",
|
||||
"endPreview": "Akhiri Pratinjau",
|
||||
"selectAgain": "Pilih lagi",
|
||||
"myItems": "Barang saya",
|
||||
"use": "Menggunakan",
|
||||
"unUse": "Buka perlengkapan",
|
||||
"renewal": "Pembaruan",
|
||||
"wallet": "Dompet",
|
||||
"profile": "Profil",
|
||||
"giftwall": "dinding hadiah",
|
||||
"announcement": "Pengumuman",
|
||||
"blockedList": "Daftar yang diblokir",
|
||||
"country2": "Negara:",
|
||||
"sendTo": "Kirim ke",
|
||||
"credits": "Kredit: {1}",
|
||||
"successfullyUnloaded": "Berhasil dibongkar",
|
||||
"expired": "Kedaluwarsa",
|
||||
"day": "Hari",
|
||||
"inUse": "Sedang digunakan",
|
||||
"staticOrGifImage": "Gambar statis / GIF",
|
||||
"confirmUseTips": "Apakah Anda ingin mengonfirmasi penggunaannya?",
|
||||
"pleaseUploadUserAvatar": "Silakan unggah avatar.",
|
||||
"joinMemberTips": "Jika Anda adalah pengunjung ruangan, Anda tidak dapat mengambil mikrofon.",
|
||||
"giftGivingSuccessful": "Pemberian hadiah berhasil.",
|
||||
"theAccountPasswordCannotBeEmpty": "Akun atau kata sandi tidak boleh kosong.",
|
||||
"invitesYouToTheMicrophone": "{1} mengundang Anda ke mikrofon",
|
||||
"english": "Bahasa inggris",
|
||||
"chinese": "Cina",
|
||||
"arabic": "Arab",
|
||||
"darkMode": "Mode Gelap",
|
||||
"lightMode": "Modus Cahaya",
|
||||
"systemDefault": "Bawaan Sistem",
|
||||
"pleaseGetOnTheMicFirst": "Silakan nyalakan mikrofonnya terlebih dahulu.",
|
||||
"roomMusicAddMusic": "Tambahkan Musik",
|
||||
"roomMusicSelectAll": "Pilih semua",
|
||||
"roomMusicAddedCount": "Menambahkan {1} lagu",
|
||||
"roomMusicEmpty": "Tidak ada musik",
|
||||
"roomMusicScanFromPhone": "Pindai dari ponsel",
|
||||
"roomMusicTotalSongs": "Jumlah: {1} lagu",
|
||||
"roomMusicNotFound": "Tidak ada musik yang ditemukan",
|
||||
"roomMusicDeleteConfirm": "Hapus musik ini?",
|
||||
"roomMusicDeleted": "Musik dihapus",
|
||||
"roomMusicListMode": "Mainkan secara berurutan",
|
||||
"roomMusicShuffleMode": "Putar acak",
|
||||
"roomMusicSingleMode": "Lingkaran tunggal",
|
||||
"duration2": "Durasi:{1}",
|
||||
"roomRocketNoteTitle": "Catatan",
|
||||
"roomRocketNoteSectionTitle": "Setiap kali VIP",
|
||||
"roomRocketNoteSectionBody": "1. Setiap kali level VIP dibeli, level tersebut berlaku selama 30 hari, dan dapat diperpanjang secara manual setelah habis masa berlakunya.",
|
||||
"roomRocketRewardEmptyText": "Tidak ada imbalan",
|
||||
"roomRocketRecordEmptyText": "Tidak ada catatan hadiah",
|
||||
"launchRocket": "Luncurkan Roket",
|
||||
"roomRocketFuelFullNotice": "Bahan bakar roket sudah penuh! Diluncurkan dalam waktu {1} detik!"
|
||||
}
|
||||
@ -31,8 +31,8 @@
|
||||
<string>yumi</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@ -57,8 +57,8 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>102</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>We need access to your camera so you can take a photo and make it a profile picture, or take and share a picture in real time in a voice chat room.</string>
|
||||
|
||||
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
defaultValue: 'http://192.168.110.64:1100/',
|
||||
); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/”
|
||||
|
||||
@override
|
||||
|
||||
@ -16,8 +16,6 @@ class SCAppLocalizations {
|
||||
'it',
|
||||
'pt',
|
||||
'fr',
|
||||
'id',
|
||||
'es',
|
||||
];
|
||||
|
||||
static const List<Locale> supportedLocales = [
|
||||
@ -28,8 +26,6 @@ class SCAppLocalizations {
|
||||
Locale('it', ''),
|
||||
Locale('pt', ''),
|
||||
Locale('fr', ''),
|
||||
Locale('id', ''),
|
||||
Locale('es', ''),
|
||||
];
|
||||
|
||||
static bool isSupportedLanguageCode(String languageCode) {
|
||||
|
||||
@ -1,180 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:pag/pag.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const LuckyGiftGlobalFloatingPreviewApp());
|
||||
}
|
||||
|
||||
class LuckyGiftGlobalFloatingPreviewApp extends StatelessWidget {
|
||||
const LuckyGiftGlobalFloatingPreviewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
minTextAdapt: true,
|
||||
builder: (_, child) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: const _LuckyGiftGlobalFloatingPreviewPage(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LuckyGiftGlobalFloatingPreviewPage extends StatelessWidget {
|
||||
const _LuckyGiftGlobalFloatingPreviewPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(_backgroundAsset, fit: BoxFit.cover),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 116.w,
|
||||
child: FloatingLuckyGiftGlobalScreenWidget(
|
||||
debugDisableAutoSlide: true,
|
||||
message: SCFloatingMessage(
|
||||
type: FloatingLuckyGiftGlobalScreenWidget.floatingType,
|
||||
roomId: '1002900',
|
||||
userName: 'Bajul',
|
||||
userAvatarUrl: '',
|
||||
coins: 1000,
|
||||
multiple: 10,
|
||||
),
|
||||
onAnimationCompleted: () {},
|
||||
),
|
||||
),
|
||||
Positioned(left: 0, top: 250.w, child: const _PagLayerScanner()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _backgroundAsset = 'sc_images/room/background_examples/bg_example_1.png';
|
||||
|
||||
class _PagLayerScanner extends StatefulWidget {
|
||||
const _PagLayerScanner();
|
||||
|
||||
@override
|
||||
State<_PagLayerScanner> createState() => _PagLayerScannerState();
|
||||
}
|
||||
|
||||
class _PagLayerScannerState extends State<_PagLayerScanner> {
|
||||
final _pagKey = GlobalKey<PAGViewState>();
|
||||
final _foundLayers = <String>{};
|
||||
String _summary = 'loading PAG...';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: 160.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
child: PAGView.asset(
|
||||
FloatingLuckyGiftGlobalScreenWidget.backgroundPagAssetPath,
|
||||
key: _pagKey,
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
autoPlay: false,
|
||||
initProgress: 0.35,
|
||||
repeatCount: PAGView.REPEAT_COUNT_LOOP,
|
||||
onInit: _scanLayers,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
padding: EdgeInsets.all(6.w),
|
||||
color: Colors.black.withValues(alpha: 0.65),
|
||||
child: Text(
|
||||
_summary,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9.sp,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _scanLayers() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
final state = _pagKey.currentState;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final rawWidth = state.rawWidth;
|
||||
final rawHeight = state.rawHeight;
|
||||
debugPrint('[LuckyGiftPAG] rawSize=${rawWidth}x$rawHeight');
|
||||
if (rawWidth <= 0 || rawHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
final sampleXs =
|
||||
<double>[
|
||||
40,
|
||||
80,
|
||||
120,
|
||||
170,
|
||||
220,
|
||||
280,
|
||||
340,
|
||||
410,
|
||||
480,
|
||||
560,
|
||||
640,
|
||||
710,
|
||||
].where((value) => value < rawWidth).toList();
|
||||
final sampleYs =
|
||||
<double>[
|
||||
32,
|
||||
58,
|
||||
82,
|
||||
104,
|
||||
128,
|
||||
158,
|
||||
].where((value) => value < rawHeight).toList();
|
||||
for (final y in sampleYs) {
|
||||
for (final x in sampleXs) {
|
||||
final layers = await state.getLayersUnderPoint(x, y);
|
||||
if (layers.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
_foundLayers.addAll(layers.where((name) => name.trim().isNotEmpty));
|
||||
debugPrint(
|
||||
'[LuckyGiftPAG] point=(${x.toStringAsFixed(0)},'
|
||||
'${y.toStringAsFixed(0)}) layers=$layers',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_summary =
|
||||
'raw ${rawWidth.toStringAsFixed(0)}x'
|
||||
'${rawHeight.toStringAsFixed(0)} layers: '
|
||||
'${_foundLayers.join(', ')}';
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,6 @@ import 'package:yumi/shared/tools/sc_date_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_user_id_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_chat_bubble_message_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
@ -57,6 +56,7 @@ import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
import 'package:yumi/shared/tools/sc_message_notifier.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||
import 'package:yumi/modules/index/main_route.dart';
|
||||
@ -581,14 +581,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
// 监听消息扩展更新
|
||||
onRecvMessageExtensionsChanged: (msgID, extensions) {
|
||||
for (var ext in extensions) {
|
||||
if (ext.extensionKey == scChatBubbleMessageExtensionKey) {
|
||||
_MessageItem.updateChatBubbleExtensionCache(
|
||||
msgID,
|
||||
ext.extensionValue,
|
||||
);
|
||||
setState(() {});
|
||||
continue;
|
||||
}
|
||||
if (ext.extensionKey == "packetID") {
|
||||
String packetID = ext.extensionValue;
|
||||
SCMessageUtils.redPacketFutureCache[packetID] =
|
||||
@ -993,9 +985,6 @@ class _SCMessageChatPageState extends State<SCMessageChatPage> {
|
||||
|
||||
class _MessageItem extends StatelessWidget {
|
||||
static bool _isOpeningCpInviteDialog = false;
|
||||
static final Map<String, Future<PropsResources?>>
|
||||
_chatBubbleExtensionFutures = {};
|
||||
static final Map<String, PropsResources?> _chatBubbleExtensionCache = {};
|
||||
|
||||
final V2TimMessage message;
|
||||
final V2TimMessage? preMessage;
|
||||
@ -2634,81 +2623,45 @@ class _MessageItem extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildTextBubble(String content) {
|
||||
final chatBubble = scDecodeChatBubbleFromCloudCustomData(
|
||||
message.cloudCustomData,
|
||||
);
|
||||
final chatBubble =
|
||||
(message.isSelf ?? false)
|
||||
? _currentUserChatBubble()
|
||||
: _senderProfile()?.getChatBox();
|
||||
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
|
||||
if (chatBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
|
||||
}
|
||||
final msgID = message.msgID?.trim();
|
||||
if (msgID == null ||
|
||||
msgID.isEmpty ||
|
||||
message.elemType != MessageElemType.V2TIM_ELEM_TYPE_TEXT) {
|
||||
return _buildDefaultTextBubble(content);
|
||||
}
|
||||
return FutureBuilder<PropsResources?>(
|
||||
future: _chatBubbleFromMessageExtension(message),
|
||||
builder: (_, snapshot) {
|
||||
final extensionBubbleUrl = _resolveChatBubbleImageUrl(snapshot.data);
|
||||
if (extensionBubbleUrl.isNotEmpty) {
|
||||
return _buildVipTextBubble(
|
||||
content: content,
|
||||
imageUrl: extensionBubbleUrl,
|
||||
);
|
||||
}
|
||||
return _buildDefaultTextBubble(content);
|
||||
},
|
||||
);
|
||||
return _buildDefaultTextBubble(content);
|
||||
}
|
||||
|
||||
static void updateChatBubbleExtensionCache(
|
||||
String msgID,
|
||||
String extensionValue,
|
||||
) {
|
||||
final id = msgID.trim();
|
||||
if (id.isEmpty) {
|
||||
return;
|
||||
PropsResources? _currentUserChatBubble() {
|
||||
final currentUser = AccountStorage().getCurrentUser();
|
||||
final memoryBubble = currentUser?.userProfile?.getChatBox();
|
||||
if (_resolveChatBubbleImageUrl(memoryBubble).isNotEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
if (currentUser?.userProfile != null) {
|
||||
return memoryBubble;
|
||||
}
|
||||
|
||||
final userJson = DataPersistence.getCurrentUser();
|
||||
if (userJson.isEmpty) {
|
||||
return memoryBubble;
|
||||
}
|
||||
try {
|
||||
return SocialChatLoginRes.fromJson(
|
||||
jsonDecode(userJson),
|
||||
).userProfile?.getChatBox();
|
||||
} catch (_) {
|
||||
return memoryBubble;
|
||||
}
|
||||
_chatBubbleExtensionCache[id] = scDecodeChatBubbleSnapshotValue(
|
||||
extensionValue,
|
||||
);
|
||||
_chatBubbleExtensionFutures.remove(id);
|
||||
}
|
||||
|
||||
Future<PropsResources?> _chatBubbleFromMessageExtension(
|
||||
V2TimMessage message,
|
||||
) {
|
||||
final msgID = message.msgID?.trim();
|
||||
if (msgID == null || msgID.isEmpty) {
|
||||
return Future.value(null);
|
||||
SocialChatUserProfile? _senderProfile() {
|
||||
if (message.isSelf ?? false) {
|
||||
return AccountStorage().getCurrentUser()?.userProfile;
|
||||
}
|
||||
if (_chatBubbleExtensionCache.containsKey(msgID)) {
|
||||
return Future.value(_chatBubbleExtensionCache[msgID]);
|
||||
}
|
||||
return _chatBubbleExtensionFutures.putIfAbsent(msgID, () async {
|
||||
if (message.isSupportMessageExtension == false) {
|
||||
_chatBubbleExtensionCache[msgID] = null;
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final result = await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.getMessageExtensions(msgID: msgID);
|
||||
final extensions = result.data ?? const <V2TimMessageExtension>[];
|
||||
for (final extension in extensions) {
|
||||
if (extension.extensionKey == scChatBubbleMessageExtensionKey) {
|
||||
final bubble = scDecodeChatBubbleSnapshotValue(
|
||||
extension.extensionValue,
|
||||
);
|
||||
_chatBubbleExtensionCache[msgID] = bubble;
|
||||
return bubble;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
_chatBubbleExtensionCache[msgID] = null;
|
||||
return null;
|
||||
});
|
||||
return friend;
|
||||
}
|
||||
|
||||
Widget _buildDefaultTextBubble(String content) {
|
||||
|
||||
@ -137,6 +137,60 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return 'type=${error.runtimeType} error=$error';
|
||||
}
|
||||
|
||||
void _showLocalLuckyGiftFeedback(
|
||||
List<MicRes> acceptUsers, {
|
||||
required SocialChatGiftRes gift,
|
||||
required int quantity,
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) {
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
final rtmProvider = Provider.of<RtmProvider>(
|
||||
navigatorKey.currentState!.context,
|
||||
listen: false,
|
||||
);
|
||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||
if (currentUser == null) {
|
||||
_giftFxLog(
|
||||
'local lucky feedback skipped reason=no_current_user giftId=${gift.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (final acceptUser in acceptUsers) {
|
||||
final targetUser = acceptUser.user;
|
||||
if (targetUser == null) {
|
||||
_giftFxLog(
|
||||
'local lucky feedback skipped reason=no_target_user giftId=${gift.id}',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
_giftFxLog(
|
||||
'local lucky feedback trigger '
|
||||
'giftId=${gift.id} '
|
||||
'giftName=${gift.giftName} '
|
||||
'toUserId=${targetUser.id} '
|
||||
'toUserName=${targetUser.userNickname} '
|
||||
'quantity=$quantity',
|
||||
);
|
||||
rtmProvider.msgFloatingGiftListener?.call(
|
||||
Msg(
|
||||
groupId:
|
||||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||||
msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
type: SCRoomMsgType.luckGiftAnimOther,
|
||||
gift: gift,
|
||||
user: currentUser,
|
||||
toUser: targetUser,
|
||||
targetUserIds: targetUserIds,
|
||||
giftBatchId: giftBatchId,
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) {
|
||||
checkedGift = gift;
|
||||
if (gift != null &&
|
||||
@ -1547,6 +1601,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
_showLocalCpInviteWaitingAfterGift(request, request.acceptUsers);
|
||||
if (request.isLuckyGiftRequest) {
|
||||
final giftBatchId = _createGiftBatchId(request);
|
||||
_showLocalLuckyGiftFeedback(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
quantity: request.quantity,
|
||||
giftBatchId: giftBatchId,
|
||||
animationCount: request.clickCount,
|
||||
);
|
||||
await sendLuckGiftAnimOtherMsg(
|
||||
request.acceptUsers,
|
||||
gift: request.gift,
|
||||
@ -1760,42 +1821,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
'hasFullScreenEffect=$hasFullScreenEffect '
|
||||
'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}',
|
||||
);
|
||||
if (gift.giftSourceUrl != null && gift.special != null) {
|
||||
if (scGiftHasFullScreenEffect(gift.special)) {
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_giftFxLog(
|
||||
'local trigger player play '
|
||||
'path=${gift.giftSourceUrl} '
|
||||
'giftId=${gift.id} '
|
||||
'giftName=${gift.giftName} '
|
||||
'animationCount=$animationCount',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because visual effects disabled '
|
||||
'giftId=${gift.id} '
|
||||
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
|
||||
'roomVisible=${rtcProvider?.shouldShowRoomVisualEffects ?? false}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because special does not include '
|
||||
'${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} '
|
||||
'giftId=${gift.id} special=${gift.special}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because giftSourceUrl or special is null '
|
||||
'giftId=${gift.id} '
|
||||
'giftSourceUrl=${gift.giftSourceUrl} '
|
||||
'special=${gift.special}',
|
||||
);
|
||||
}
|
||||
Provider.of<RtmProvider>(
|
||||
navigatorKey.currentState!.context,
|
||||
listen: false,
|
||||
@ -1861,6 +1886,42 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (gift.giftSourceUrl != null && gift.special != null) {
|
||||
if (scGiftHasFullScreenEffect(gift.special)) {
|
||||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||||
SCGlobalConfig.isGiftSpecialEffects &&
|
||||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||||
_giftFxLog(
|
||||
'local trigger player play '
|
||||
'path=${gift.giftSourceUrl} '
|
||||
'giftId=${gift.id} '
|
||||
'giftName=${gift.giftName} '
|
||||
'animationCount=$animationCount',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because visual effects disabled '
|
||||
'giftId=${gift.id} '
|
||||
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
|
||||
'roomVisible=${rtcProvider?.shouldShowRoomVisualEffects ?? false}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because special does not include '
|
||||
'${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} '
|
||||
'giftId=${gift.id} special=${gift.special}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip local play because giftSourceUrl or special is null '
|
||||
'giftId=${gift.id} '
|
||||
'giftSourceUrl=${gift.giftSourceUrl} '
|
||||
'special=${gift.special}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2065,7 +2126,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
required String giftBatchId,
|
||||
int animationCount = 1,
|
||||
}) async {
|
||||
final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers);
|
||||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||
final firstTargetUser =
|
||||
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
||||
_giftFxLog(
|
||||
@ -2094,7 +2155,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
number: quantity,
|
||||
customAnimationCount: animationCount,
|
||||
type: SCRoomMsgType.luckGiftAnimOther,
|
||||
msg: jsonEncode(targetUserIds),
|
||||
msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()),
|
||||
),
|
||||
addLocal: true,
|
||||
);
|
||||
@ -2117,26 +2178,6 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
List<String> _resolveAcceptUserIdentityKeys(List<MicRes> acceptUsers) {
|
||||
final targetUserIds = <String>[];
|
||||
void addTargetUserId(String? userId) {
|
||||
final normalizedUserId = (userId ?? "").trim();
|
||||
if (normalizedUserId.isEmpty ||
|
||||
targetUserIds.contains(normalizedUserId)) {
|
||||
return;
|
||||
}
|
||||
targetUserIds.add(normalizedUserId);
|
||||
}
|
||||
|
||||
for (final acceptUser in acceptUsers) {
|
||||
final user = acceptUser.user;
|
||||
addTargetUserId(user?.id);
|
||||
addTargetUserId(user?.account);
|
||||
addTargetUserId(user?.getID());
|
||||
}
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
String _createGiftBatchId(_GiftSendRequest request) {
|
||||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||||
..sort();
|
||||
|
||||
@ -441,12 +441,12 @@ class _SCIndexPageState extends State<SCIndexPage>
|
||||
return;
|
||||
}
|
||||
|
||||
await _showEntryPopupIfNeeded();
|
||||
await _showDailySignInDialogIfNeeded();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _showDailySignInDialogIfNeeded();
|
||||
await _showEntryPopupIfNeeded();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -619,12 +619,15 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
if (path.isEmpty) {
|
||||
return SizedBox(width: width, height: width);
|
||||
}
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: width,
|
||||
);
|
||||
if (path.startsWith("assets/") || path.startsWith("sc_images/")) {
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: width,
|
||||
);
|
||||
}
|
||||
return netImage(url: path, width: width);
|
||||
}
|
||||
|
||||
void _checkStart() {
|
||||
|
||||
@ -175,7 +175,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
void _handleRoomRouteVisible() {
|
||||
_releaseCoveredRouteGuard();
|
||||
_setRoomRouteVisible(true);
|
||||
_scheduleEnsureRoomVisualEffectsEnabled();
|
||||
}
|
||||
|
||||
void _scheduleRoomRouteVisible() {
|
||||
@ -250,20 +249,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
rtmProvider.msgFloatingGiftListener = _floatingGiftListener;
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener =
|
||||
_luckyGiftRewardTickerListener;
|
||||
debugPrint('[GiftFx][voice] bind room visual effect listeners');
|
||||
}
|
||||
|
||||
void _ensureRoomVisualEffectsEnabled() {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (rtcProvider.currenRoom == null || !_roomRouteVisible) {
|
||||
return;
|
||||
}
|
||||
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||
final needsListenerRebind =
|
||||
rtmProvider.msgFloatingGiftListener != _floatingGiftListener ||
|
||||
rtmProvider.msgLuckyGiftRewardTickerListener !=
|
||||
_luckyGiftRewardTickerListener;
|
||||
if (rtcProvider.roomVisualEffectsEnabled && !needsListenerRebind) {
|
||||
if (rtcProvider.currenRoom == null ||
|
||||
!_roomRouteVisible ||
|
||||
rtcProvider.roomVisualEffectsEnabled) {
|
||||
return;
|
||||
}
|
||||
_enableRoomVisualEffects();
|
||||
@ -694,44 +686,18 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_floatingGiftListener(Msg msg) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
if (!rtcProvider.shouldShowRoomVisualEffects) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip floating listener visual effects hidden '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'enabled=${rtcProvider.roomVisualEffectsEnabled} '
|
||||
'visible=${rtcProvider.isVoiceRoomRouteVisible} '
|
||||
'hasRoom=${rtcProvider.currenRoom != null}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (Provider.of<GiftProvider>(context, listen: false).hideLGiftAnimal) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip floating listener hidden by gift preference '
|
||||
'type=${msg.type} giftId=${msg.gift?.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
||||
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
|
||||
debugPrint(
|
||||
'[GiftFx][voice] floating listener recv '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftTab=${msg.gift?.giftTab} '
|
||||
'quantity=${msg.number} '
|
||||
'animationCount=${msg.customAnimationCount} '
|
||||
'giftPhoto=${msg.gift?.giftPhoto} '
|
||||
'targetUserIds=${targetUserIds.join(",")} '
|
||||
'targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
final shouldPlayBatchVisuals =
|
||||
targetUserIds.length <= 1 ||
|
||||
targetGroupKey == null ||
|
||||
_markGiftVisualBatchForPlayback(msg, targetGroupKey);
|
||||
if (shouldPlayBatchVisuals) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
var giftModel = LGiftModel();
|
||||
giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey);
|
||||
giftModel.sendUserName = msg.user?.userNickname ?? "";
|
||||
@ -762,10 +728,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
listen: false,
|
||||
).enqueueGiftAnimation(giftModel);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip gift ticker by visual batch dedupe '
|
||||
'type=${msg.type} giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||
@ -781,7 +744,14 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
targetGroupKey,
|
||||
targetUserIds,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_handleStandardGiftComboVisuals(
|
||||
msg,
|
||||
giftPhoto,
|
||||
targetGroupKey,
|
||||
targetUserIds,
|
||||
);
|
||||
}
|
||||
|
||||
void _enqueueCpGiftFloatingIfNeeded(Msg msg) {
|
||||
@ -893,9 +863,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
|
||||
bool _isLuckyGiftMessage(Msg msg) {
|
||||
final giftTab = (msg.gift?.giftTab ?? '').trim();
|
||||
return giftTab == "LUCK" ||
|
||||
giftTab == SCGiftType.LUCKY_GIFT.name ||
|
||||
giftTab == SCGiftType.MAGIC.name;
|
||||
return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name;
|
||||
}
|
||||
|
||||
bool _supportsComboMilestoneEffects(Msg msg) {
|
||||
@ -944,24 +912,24 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
}
|
||||
}
|
||||
|
||||
void _handleLuckyGiftComboVisuals(
|
||||
void _handleStandardGiftComboVisuals(
|
||||
Msg msg,
|
||||
String giftPhoto,
|
||||
String? targetGroupKey,
|
||||
List<String> targetUserIds,
|
||||
) {
|
||||
if ((msg.number ?? 0) <= 0) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight invalid quantity '
|
||||
'giftId=${msg.gift?.id} quantity=${msg.number}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight empty targets '
|
||||
'giftId=${msg.gift?.id} targetGroupKey=$targetGroupKey',
|
||||
);
|
||||
if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
|
||||
targetGroupKey == null ||
|
||||
targetUserIds.isEmpty ||
|
||||
giftPhoto.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||
if (targetUserIds.length > 1 &&
|
||||
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||
return;
|
||||
}
|
||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||
@ -972,25 +940,44 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
session.endTimer?.cancel();
|
||||
session.clearQueueTimer?.cancel();
|
||||
|
||||
if (_shouldPlayLuckyGiftSeatFlightAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||
_enqueueTrackedSeatFlightAnimations(
|
||||
sessionKey: sessionKey,
|
||||
giftPhoto: giftPhoto,
|
||||
targetUserIds: targetUserIds,
|
||||
animationCount: _resolveTrackedAnimationCount(msg),
|
||||
batchKey: flightBatchKey,
|
||||
);
|
||||
|
||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||
}
|
||||
|
||||
void _handleLuckyGiftComboVisuals(
|
||||
Msg msg,
|
||||
String giftPhoto,
|
||||
String? targetGroupKey,
|
||||
List<String> targetUserIds,
|
||||
) {
|
||||
if ((msg.number ?? 0) <= 0) {
|
||||
return;
|
||||
}
|
||||
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||
final session = _luckyGiftComboSessions[sessionKey];
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
session.endTimer?.cancel();
|
||||
session.clearQueueTimer?.cancel();
|
||||
|
||||
if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||
if (targetUserIds.length > 1 &&
|
||||
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight by flight batch dedupe '
|
||||
'giftId=${msg.gift?.id} batchKey=$flightBatchKey',
|
||||
);
|
||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'[GiftFx][voice] enqueue lucky seat flight '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftPhoto=$giftPhoto '
|
||||
'targetUserIds=${targetUserIds.join(",")} '
|
||||
'animationCount=${_resolveTrackedAnimationCount(msg)} '
|
||||
'batchKey=$flightBatchKey',
|
||||
);
|
||||
_enqueueTrackedSeatFlightAnimations(
|
||||
sessionKey: sessionKey,
|
||||
giftPhoto: giftPhoto,
|
||||
@ -998,29 +985,10 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
animationCount: _resolveTrackedAnimationCount(msg),
|
||||
batchKey: flightBatchKey,
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip lucky seat flight unsupported gift photo '
|
||||
'giftId=${msg.gift?.id} giftPhoto=$giftPhoto',
|
||||
);
|
||||
}
|
||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||
}
|
||||
|
||||
bool _shouldPlayLuckyGiftSeatFlightAnimation(Msg msg) {
|
||||
final gift = msg.gift;
|
||||
if (gift == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftPhoto = (gift.giftPhoto ?? "").trim();
|
||||
if (giftPhoto.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto);
|
||||
return !_isAnimatedGiftResource(giftPhotoExt);
|
||||
}
|
||||
|
||||
int _resolveTrackedAnimationCount(Msg msg) {
|
||||
return math.max(msg.customAnimationCount ?? 1, 1);
|
||||
}
|
||||
@ -1050,51 +1018,21 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
);
|
||||
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||
if (normalizedTargetUserIds.isEmpty) {
|
||||
debugPrint(
|
||||
'[GiftFx][voice] skip seat flight enqueue empty normalized targets '
|
||||
'sessionKey=$sessionKey',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
final seenSeatKeys = <GlobalKey<State<StatefulWidget>>>{};
|
||||
final resolvedTargetUserIds = <String>[];
|
||||
final unresolvedTargetUserIds = <String>[];
|
||||
for (final targetUserId in normalizedTargetUserIds) {
|
||||
final targetKey = rtcProvider.getSeatGlobalKeyByIndex(targetUserId);
|
||||
if (targetKey == null) {
|
||||
unresolvedTargetUserIds.add(targetUserId);
|
||||
continue;
|
||||
}
|
||||
if (seenSeatKeys.add(targetKey)) {
|
||||
resolvedTargetUserIds.add(targetUserId);
|
||||
}
|
||||
}
|
||||
final flightTargetUserIds =
|
||||
resolvedTargetUserIds.isNotEmpty
|
||||
? resolvedTargetUserIds
|
||||
: normalizedTargetUserIds;
|
||||
debugPrint(
|
||||
'[GiftFx][voice] seat flight targets '
|
||||
'sessionKey=$sessionKey '
|
||||
'requested=${normalizedTargetUserIds.join(",")} '
|
||||
'resolved=${resolvedTargetUserIds.join(",")} '
|
||||
'unresolved=${unresolvedTargetUserIds.join(",")} '
|
||||
'used=${flightTargetUserIds.join(",")}',
|
||||
);
|
||||
final normalizedAnimationCount = math.max(animationCount, 1);
|
||||
final cappedAnimationCount = math.min(
|
||||
normalizedAnimationCount,
|
||||
_maxTrackedGiftAnimations,
|
||||
);
|
||||
final maxTrackedRequests = math.max(
|
||||
_maxTrackedGiftAnimations * flightTargetUserIds.length,
|
||||
_maxTrackedGiftAnimations * normalizedTargetUserIds.length,
|
||||
_maxTrackedGiftAnimations,
|
||||
);
|
||||
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
||||
final roundBatchTag =
|
||||
flightTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||
for (final targetUserId in flightTargetUserIds) {
|
||||
normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||
for (final targetUserId in normalizedTargetUserIds) {
|
||||
_giftSeatFlightController.enqueueLimited(
|
||||
RoomGiftSeatFlightRequest(
|
||||
imagePath: giftPhoto,
|
||||
@ -1316,6 +1254,26 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
_giftVisualBatchDedupeTimers.clear();
|
||||
}
|
||||
|
||||
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
|
||||
final gift = msg.gift;
|
||||
if (gift == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftPhoto = (gift.giftPhoto ?? "").trim();
|
||||
if (giftPhoto.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto);
|
||||
if (_isAnimatedGiftResource(giftPhotoExt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final giftSourceUrl = (gift.giftSourceUrl ?? "").trim();
|
||||
final sourceExt = _normalizedGiftResourceExtension(giftSourceUrl);
|
||||
return !_isAnimatedGiftResource(sourceExt);
|
||||
}
|
||||
|
||||
bool _isAnimatedGiftResource(String extension) {
|
||||
return extension == ".svga" || extension == ".mp4" || extension == ".vap";
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ class BaishunBridgeActions {
|
||||
static const String gameLoaded = 'gameLoaded';
|
||||
static const String gameRecharge = 'gameRecharge';
|
||||
static const String destroy = 'destroy';
|
||||
static const String debugLog = 'debugLog';
|
||||
}
|
||||
|
||||
class BaishunBridgeMessage {
|
||||
@ -103,6 +104,45 @@ class BaishunJsBridge {
|
||||
$channelName.postMessage(JSON.stringify({ action: action, payload: payload }));
|
||||
return payload;
|
||||
}
|
||||
function clipText(value, limit) {
|
||||
const text = (value == null ? '' : String(value)).trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
return text.length > limit ? text.slice(0, limit) + '...' : text;
|
||||
}
|
||||
function stringifyForLog(value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return clipText(value, 320);
|
||||
}
|
||||
try {
|
||||
return clipText(JSON.stringify(value), 320);
|
||||
} catch (_) {
|
||||
return clipText(String(value), 320);
|
||||
}
|
||||
}
|
||||
function postDebug(tag, payload) {
|
||||
$channelName.postMessage(JSON.stringify({
|
||||
action: '${BaishunBridgeActions.debugLog}',
|
||||
payload: {
|
||||
tag: tag,
|
||||
message: stringifyForLog(payload)
|
||||
}
|
||||
}));
|
||||
}
|
||||
function maskText(value) {
|
||||
const text = (value == null ? '' : String(value)).trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= 10) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, 6) + '***' + text.slice(-4);
|
||||
}
|
||||
function ensureChannelAlias(name, handler) {
|
||||
if (!window[name] || typeof window[name].postMessage !== 'function') {
|
||||
window[name] = { postMessage: handler };
|
||||
@ -116,21 +156,30 @@ class BaishunJsBridge {
|
||||
}
|
||||
}
|
||||
window.NativeBridge.getConfigSync = function() {
|
||||
postDebug('bridge.getConfigSync', {
|
||||
hasConfig: !!window.__baishunLastConfig,
|
||||
hasNativeBridgeConfig: !!(window.NativeBridge && window.NativeBridge.config)
|
||||
});
|
||||
return window.__baishunLastConfig || null;
|
||||
};
|
||||
window.NativeBridge.getConfig = function(params) {
|
||||
postDebug('bridge.getConfig.call', toPayload(params));
|
||||
postAction('${BaishunBridgeActions.getConfig}', params);
|
||||
return window.__baishunLastConfig || null;
|
||||
};
|
||||
window.NativeBridge.destroy = function(payload) {
|
||||
postDebug('bridge.destroy.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.destroy}', payload);
|
||||
};
|
||||
window.NativeBridge.gameRecharge = function(payload) {
|
||||
postDebug('bridge.gameRecharge.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.gameRecharge}', payload);
|
||||
};
|
||||
window.NativeBridge.gameLoaded = function(payload) {
|
||||
postDebug('bridge.gameLoaded.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.gameLoaded}', payload);
|
||||
};
|
||||
window.__baishunDebugLog = postDebug;
|
||||
ensureChannelAlias('getConfig', function(message) {
|
||||
window.NativeBridge.getConfig(message);
|
||||
});
|
||||
@ -155,6 +204,163 @@ class BaishunJsBridge {
|
||||
ensureWebkitHandler('gameLoaded', function(message) {
|
||||
window.NativeBridge.gameLoaded(message);
|
||||
});
|
||||
if (!window.__baishunNetworkDebugReady) {
|
||||
window.__baishunNetworkDebugReady = true;
|
||||
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function' && !window.__baishunInputDebugReady) {
|
||||
window.__baishunInputDebugReady = true;
|
||||
window.__baishunInputDebugCount = 0;
|
||||
['pointerdown', 'touchstart', 'mousedown', 'click'].forEach(function(eventName) {
|
||||
document.addEventListener(eventName, function(event) {
|
||||
if ((window.__baishunInputDebugCount || 0) >= 24) {
|
||||
return;
|
||||
}
|
||||
window.__baishunInputDebugCount = (window.__baishunInputDebugCount || 0) + 1;
|
||||
const target = event && event.target;
|
||||
const touch = event && event.touches && event.touches.length ? event.touches[0] : null;
|
||||
postDebug('input.' + eventName, {
|
||||
x: touch ? touch.clientX : (event && event.clientX),
|
||||
y: touch ? touch.clientY : (event && event.clientY),
|
||||
pointerType: event && event.pointerType,
|
||||
targetTag: target && target.tagName,
|
||||
targetId: target && target.id,
|
||||
targetClass: target && target.className
|
||||
});
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
if (typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('error', function(event) {
|
||||
const target = event && event.target;
|
||||
if (target && target !== window) {
|
||||
postDebug('resource.error', {
|
||||
tag: target.tagName || '',
|
||||
source: target.currentSrc || target.src || target.href || '',
|
||||
outerHTML: target.outerHTML || ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
postDebug('window.error', {
|
||||
message: event && event.message,
|
||||
source: event && event.filename,
|
||||
line: event && event.lineno,
|
||||
column: event && event.colno,
|
||||
stack: event && event.error && event.error.stack
|
||||
});
|
||||
}, true);
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
postDebug('promise.reject', {
|
||||
reason: event && event.reason
|
||||
});
|
||||
});
|
||||
}
|
||||
if (window.console && !window.__baishunConsoleDebugReady) {
|
||||
window.__baishunConsoleDebugReady = true;
|
||||
['log', 'info', 'warn', 'error'].forEach(function(level) {
|
||||
const original = typeof window.console[level] === 'function'
|
||||
? window.console[level].bind(window.console)
|
||||
: null;
|
||||
window.console[level] = function() {
|
||||
try {
|
||||
postDebug('console.' + level, Array.prototype.slice.call(arguments));
|
||||
} catch (_) {}
|
||||
if (original) {
|
||||
return original.apply(window.console, arguments);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
});
|
||||
}
|
||||
if (typeof window.fetch === 'function') {
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = function(input, init) {
|
||||
const url = typeof input === 'string' ? input : (input && input.url) || '';
|
||||
const method = (init && init.method) || 'GET';
|
||||
postDebug('fetch.start', { method: method, url: url });
|
||||
return originalFetch(input, init)
|
||||
.then(function(response) {
|
||||
const cloned = response && typeof response.clone === 'function' ? response.clone() : null;
|
||||
if (cloned && typeof cloned.text === 'function') {
|
||||
cloned.text().then(function(text) {
|
||||
if (!response.ok || /failed|error|exception|get user info/i.test(text)) {
|
||||
postDebug('fetch.end', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: response.status,
|
||||
body: text
|
||||
});
|
||||
} else {
|
||||
postDebug('fetch.end', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: response.status
|
||||
});
|
||||
}
|
||||
}).catch(function(error) {
|
||||
postDebug('fetch.readError', {
|
||||
method: method,
|
||||
url: url,
|
||||
error: error && error.message ? error.message : String(error)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
postDebug('fetch.end', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: response && response.status
|
||||
});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(function(error) {
|
||||
postDebug('fetch.error', {
|
||||
method: method,
|
||||
url: url,
|
||||
error: error && error.message ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) {
|
||||
const xhrOpen = window.XMLHttpRequest.prototype.open;
|
||||
const xhrSend = window.XMLHttpRequest.prototype.send;
|
||||
window.XMLHttpRequest.prototype.open = function(method, url) {
|
||||
this.__baishunMethod = method || 'GET';
|
||||
this.__baishunUrl = url || '';
|
||||
return xhrOpen.apply(this, arguments);
|
||||
};
|
||||
window.XMLHttpRequest.prototype.send = function(body) {
|
||||
const method = this.__baishunMethod || 'GET';
|
||||
const url = this.__baishunUrl || '';
|
||||
postDebug('xhr.start', { method: method, url: url });
|
||||
this.addEventListener('loadend', function() {
|
||||
const responseText = typeof this.responseText === 'string' ? this.responseText : '';
|
||||
if (this.status >= 400 || /failed|error|exception|get user info/i.test(responseText)) {
|
||||
postDebug('xhr.end', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: this.status,
|
||||
body: responseText
|
||||
});
|
||||
} else {
|
||||
postDebug('xhr.end', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: this.status
|
||||
});
|
||||
}
|
||||
});
|
||||
this.addEventListener('error', function() {
|
||||
postDebug('xhr.error', {
|
||||
method: method,
|
||||
url: url,
|
||||
status: this.status
|
||||
});
|
||||
});
|
||||
return xhrSend.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
window.baishunChannel = window.baishunChannel || {};
|
||||
window.baishunChannel.walletUpdate = function(payload) {
|
||||
window.__baishunLastWalletPayload = payload || {};
|
||||
@ -171,6 +377,10 @@ class BaishunJsBridge {
|
||||
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
|
||||
window.dispatchEvent(new CustomEvent('baishunBridgeReady'));
|
||||
}
|
||||
postDebug('bridge.ready', {
|
||||
href: window.location && window.location.href,
|
||||
ua: navigator && navigator.userAgent
|
||||
});
|
||||
})();
|
||||
''';
|
||||
}
|
||||
@ -185,6 +395,16 @@ class BaishunJsBridge {
|
||||
(function() {
|
||||
const config = $payload;
|
||||
const explicitCallbackPath = $encodedCallbackPath;
|
||||
function maskText(value) {
|
||||
const text = (value == null ? '' : String(value)).trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= 10) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, 6) + '***' + text.slice(-4);
|
||||
}
|
||||
function resolvePath(path) {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return null;
|
||||
@ -224,6 +444,20 @@ class BaishunJsBridge {
|
||||
if (explicitCallbackPath) {
|
||||
window.__baishunLastJsCallback = explicitCallbackPath;
|
||||
}
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('config.deliver', {
|
||||
appId: config.appId,
|
||||
appChannel: config.appChannel,
|
||||
userId: config.userId,
|
||||
roomId: config.roomId,
|
||||
gameMode: config.gameMode,
|
||||
language: config.language,
|
||||
gsp: config.gsp,
|
||||
code: maskText(config.code),
|
||||
codeLength: config.code ? String(config.code).length : 0,
|
||||
callbackPath: callbackPath
|
||||
});
|
||||
}
|
||||
if (typeof window.__baishunGetConfigCallback === 'function') {
|
||||
window.__baishunGetConfigCallback(config);
|
||||
window.__baishunGetConfigCallback = null;
|
||||
@ -231,6 +465,14 @@ class BaishunJsBridge {
|
||||
if (window.__baishunLastJsCallback) {
|
||||
callbackInvoked = invokeCallbackPath(window.__baishunLastJsCallback) || callbackInvoked;
|
||||
}
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('config.callback', {
|
||||
callbackPath: callbackPath,
|
||||
callbackInvoked: callbackInvoked,
|
||||
hasLegacyCallback: typeof window.__baishunGetConfigCallback === 'function',
|
||||
hasOnBaishunConfig: typeof window.onBaishunConfig === 'function'
|
||||
});
|
||||
}
|
||||
if (typeof window.onBaishunConfig === 'function') {
|
||||
window.onBaishunConfig(config);
|
||||
}
|
||||
@ -252,6 +494,9 @@ class BaishunJsBridge {
|
||||
return '''
|
||||
(function() {
|
||||
const payload = $safePayload;
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('wallet.update', payload);
|
||||
}
|
||||
if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') {
|
||||
window.baishunChannel.walletUpdate(payload);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ class LeaderBridgeActions {
|
||||
static const String pay = 'pay';
|
||||
static const String closeGame = 'closeGame';
|
||||
static const String loadComplete = 'loadComplete';
|
||||
static const String debugLog = 'debugLog';
|
||||
}
|
||||
|
||||
class LeaderBridgeMessage {
|
||||
@ -87,6 +88,21 @@ class LeaderJsBridge {
|
||||
return {};
|
||||
}
|
||||
|
||||
function stringifyForLog(value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.length > 320 ? value.slice(0, 320) + '...' : value;
|
||||
}
|
||||
try {
|
||||
const text = JSON.stringify(value);
|
||||
return text.length > 320 ? text.slice(0, 320) + '...' : text;
|
||||
} catch (_) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function postMessage(action, input) {
|
||||
const payload = toPayload(input);
|
||||
$channelName.postMessage(JSON.stringify({
|
||||
@ -96,6 +112,13 @@ class LeaderJsBridge {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function postDebug(tag, payload) {
|
||||
postMessage('${LeaderBridgeActions.debugLog}', {
|
||||
tag: tag,
|
||||
message: stringifyForLog(payload)
|
||||
});
|
||||
}
|
||||
|
||||
function invokeCallback(callback, payload) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(payload);
|
||||
@ -176,6 +199,16 @@ class LeaderJsBridge {
|
||||
};
|
||||
if (payloadChanged) {
|
||||
dispatchConfigReady(normalizedPayload);
|
||||
postDebug('config.ready', {
|
||||
provider: normalizedPayload.provider || '',
|
||||
gameId: normalizedPayload.gameId || '',
|
||||
roomId: launchConfig.roomId || '',
|
||||
code: launchConfig.code
|
||||
? String(launchConfig.code).slice(0, 6) + '***'
|
||||
: '',
|
||||
codeLength: launchConfig.code ? String(launchConfig.code).length : 0,
|
||||
entryUrl: entry.entryUrl || ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,6 +234,12 @@ class LeaderJsBridge {
|
||||
window.NativeBridge.pay = pay;
|
||||
window.NativeBridge.closeGame = closeGame;
|
||||
window.NativeBridge.loadComplete = loadComplete;
|
||||
|
||||
window.__leaderBridgeDebug = postDebug;
|
||||
postDebug('bridge.ready', {
|
||||
href: window.location && window.location.href,
|
||||
hasUpdateCoin: typeof window.updateCoin === 'function'
|
||||
});
|
||||
}
|
||||
|
||||
applyLaunchPayload(launchPayload);
|
||||
@ -223,6 +262,13 @@ class LeaderJsBridge {
|
||||
window.onUpdateCoin();
|
||||
return;
|
||||
}
|
||||
$channelName.postMessage(JSON.stringify({
|
||||
action: '${LeaderBridgeActions.debugLog}',
|
||||
payload: {
|
||||
tag: 'game.updateCoin.missing',
|
||||
message: 'No updateCoin handler found on game page.'
|
||||
}
|
||||
}));
|
||||
})();
|
||||
''';
|
||||
}
|
||||
|
||||
@ -299,6 +299,12 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
Future<void> _dispatchBridgeMessage(
|
||||
BaishunBridgeMessage bridgeMessage,
|
||||
) async {
|
||||
if (bridgeMessage.action == BaishunBridgeActions.debugLog) {
|
||||
final tag = bridgeMessage.payload['tag']?.toString().trim();
|
||||
final message = bridgeMessage.payload['message']?.toString() ?? '';
|
||||
_log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}');
|
||||
return;
|
||||
}
|
||||
final callbackPath = _extractCallbackPath(bridgeMessage.payload);
|
||||
_log(
|
||||
'bridge_action action=${bridgeMessage.action} '
|
||||
@ -406,11 +412,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
return '';
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
@ -680,12 +682,10 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
),
|
||||
if (_errorMessage != null) _buildErrorState(),
|
||||
if (_isLoading && _errorMessage == null)
|
||||
const Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for gameLoaded...',
|
||||
),
|
||||
const IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for gameLoaded...',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -14,7 +14,7 @@ class BaishunLoadingView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.expand(
|
||||
return Positioned.fill(
|
||||
child: Container(
|
||||
color: backgroundColor,
|
||||
alignment: Alignment.center,
|
||||
|
||||
@ -245,6 +245,13 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
}
|
||||
|
||||
Future<void> _dispatchBridgeMessage(LeaderBridgeMessage bridgeMessage) async {
|
||||
if (bridgeMessage.action == LeaderBridgeActions.debugLog) {
|
||||
final tag = bridgeMessage.payload['tag']?.toString().trim();
|
||||
final message = bridgeMessage.payload['message']?.toString() ?? '';
|
||||
_log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}');
|
||||
return;
|
||||
}
|
||||
|
||||
_didReceiveBridgeMessage = true;
|
||||
_stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}');
|
||||
_log(
|
||||
@ -378,11 +385,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) {
|
||||
final trimmed = value.trim();
|
||||
@ -585,12 +588,10 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
|
||||
),
|
||||
if (_errorMessage != null) _buildErrorState(),
|
||||
if (_isLoading && _errorMessage == null)
|
||||
const Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for Leader game...',
|
||||
),
|
||||
const IgnorePointer(
|
||||
ignoring: true,
|
||||
child: BaishunLoadingView(
|
||||
message: 'Waiting for Leader game...',
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -35,7 +34,6 @@ class RoomGameListSheet extends StatefulWidget {
|
||||
|
||||
class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
static const int _itemsPerRow = 4;
|
||||
static const String _logPrefix = '[RoomGameList]';
|
||||
static const String _sheetFrameAsset =
|
||||
'sc_images/room/sc_room_game_sheet_frame.png';
|
||||
static const String _dividerLayerAsset =
|
||||
@ -196,11 +194,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('$_logPrefix $message');
|
||||
}
|
||||
}
|
||||
void _log(String message) {}
|
||||
|
||||
String _clip(String value, [int limit = 600]) {
|
||||
final trimmed = value.trim();
|
||||
|
||||
@ -32,8 +32,6 @@ class _LanguagePageState extends State<LanguagePage> {
|
||||
_LanguageOption(code: 'it', label: 'Italiano'),
|
||||
_LanguageOption(code: 'pt', label: 'Português'),
|
||||
_LanguageOption(code: 'fr', label: 'Français'),
|
||||
_LanguageOption(code: 'id', label: 'Bahasa Indonesia'),
|
||||
_LanguageOption(code: 'es', label: 'Español'),
|
||||
];
|
||||
|
||||
String _selectedLanguageCode = 'en';
|
||||
|
||||
@ -568,7 +568,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
late final SVGAAnimationController _controller;
|
||||
bool _loaded = false;
|
||||
bool _finished = false;
|
||||
String? _retainedResource;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -587,10 +586,7 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
Future<void> _loadAndPlay() async {
|
||||
try {
|
||||
final movieEntity = await _loadMovieEntity(widget.sourceUrl);
|
||||
if (!mounted || _finished) {
|
||||
_releaseRetainedResource();
|
||||
return;
|
||||
}
|
||||
if (!mounted || _finished) return;
|
||||
setState(() {
|
||||
_loaded = true;
|
||||
_controller.videoItem = movieEntity;
|
||||
@ -604,12 +600,9 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadMovieEntity(String sourceUrl) async {
|
||||
final manager = SCGiftVapSvgaManager();
|
||||
final cached = manager.getCachedSvgaEntity(sourceUrl, retain: true);
|
||||
if (cached != null) {
|
||||
_replaceRetainedResource(sourceUrl);
|
||||
return cached;
|
||||
}
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
final cached = cache[sourceUrl];
|
||||
if (cached != null) return cached;
|
||||
|
||||
final pathType = SCPathUtils.getPathType(sourceUrl);
|
||||
late final MovieEntity movieEntity;
|
||||
@ -624,35 +617,10 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
throw Exception("Unsupported SVGA source: $sourceUrl");
|
||||
}
|
||||
movieEntity.autorelease = false;
|
||||
if (!mounted || _finished) {
|
||||
movieEntity.dispose();
|
||||
return movieEntity;
|
||||
}
|
||||
manager.cacheSvgaEntity(sourceUrl, movieEntity, retain: true);
|
||||
_replaceRetainedResource(sourceUrl);
|
||||
cache[sourceUrl] = movieEntity;
|
||||
return movieEntity;
|
||||
}
|
||||
|
||||
void _replaceRetainedResource(String resource) {
|
||||
final previous = _retainedResource;
|
||||
if (previous == resource) {
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous);
|
||||
}
|
||||
_retainedResource = resource;
|
||||
}
|
||||
|
||||
void _releaseRetainedResource() {
|
||||
final resource = _retainedResource;
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource);
|
||||
}
|
||||
|
||||
void _finish() {
|
||||
if (_finished) return;
|
||||
_finished = true;
|
||||
@ -661,7 +629,6 @@ class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_releaseRetainedResource();
|
||||
_controller.removeStatusListener(_handleStatusChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
|
||||
@ -51,8 +51,6 @@ import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||||
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_mic_go_up_res.dart'
|
||||
hide PropsResources;
|
||||
import '../../shared/business_logic/models/res/sc_room_rocket_api_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_config_res.dart';
|
||||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||
@ -339,9 +337,11 @@ class _RecentMicChangeSeatSnapshot {
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const String _roomRocketPagLaunchDialogTag =
|
||||
'showRoomRocketPagLaunchDialog';
|
||||
static const Duration _roomRocketGiftRefreshThrottleDelay = Duration(
|
||||
milliseconds: 700,
|
||||
);
|
||||
static const List<Duration> _roomRocketGiftRefreshDelays = [
|
||||
Duration(milliseconds: 700),
|
||||
Duration(milliseconds: 1600),
|
||||
Duration(milliseconds: 3200),
|
||||
];
|
||||
static const List<Duration> _roomRocketPostLaunchRefreshDelays = [
|
||||
Duration(milliseconds: 800),
|
||||
Duration(seconds: 2),
|
||||
@ -406,10 +406,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _roomEntryEffectTimer;
|
||||
Timer? _roomRocketLaunchAnimationTimer;
|
||||
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
|
||||
Timer? _roomRocketGiftRefreshTimer;
|
||||
String? _roomRocketGiftRefreshRoomId;
|
||||
bool _roomRocketGiftRefreshDirty = false;
|
||||
bool _roomRocketGiftRefreshInFlight = false;
|
||||
final List<Timer> _roomRocketGiftRefreshTimers = [];
|
||||
final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
|
||||
final List<Timer> _roomRocketEntryReadyFallbackTimers = [];
|
||||
Timer? _roomRedPacketPresenceTimer;
|
||||
@ -438,7 +435,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||||
num? _pendingSelfMicReleaseIndex;
|
||||
int? _pendingSelfMicReleaseGuardUntilMs;
|
||||
final Set<String> _confirmedSelfMicIdentityKeys = <String>{};
|
||||
int? _seatResizeOccupantPreserveUntilMs;
|
||||
_RoomRtcLocalAudioRole? _lastAppliedLocalAudioRole;
|
||||
bool? _lastAppliedLocalAudioMuted;
|
||||
@ -484,24 +480,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
///禁音开关 默认关闭
|
||||
bool isMic = true;
|
||||
|
||||
bool _setSelfMicMuted(bool value) {
|
||||
if (isMic == value) {
|
||||
return false;
|
||||
}
|
||||
isMic = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _notifySelfMicUiState({bool? muted, bool notifyIfUnchanged = false}) {
|
||||
var changed = false;
|
||||
if (muted != null) {
|
||||
changed = _setSelfMicMuted(muted) || changed;
|
||||
}
|
||||
if (changed || notifyIfUnchanged) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldShowSeatSoundWave(num index) {
|
||||
final seat = roomWheatMap[index];
|
||||
final userId = (seat?.user?.id ?? "").trim();
|
||||
@ -545,44 +523,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return currentUserId.isNotEmpty && currentUserId == userId.trim();
|
||||
}
|
||||
|
||||
Set<String> _currentUserIdentityKeys() {
|
||||
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
||||
return {
|
||||
currentUser?.id?.trim() ?? "",
|
||||
currentUser?.account?.trim() ?? "",
|
||||
currentUser?.getID().trim() ?? "",
|
||||
..._confirmedSelfMicIdentityKeys,
|
||||
}..removeWhere((item) => item.isEmpty);
|
||||
}
|
||||
|
||||
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
|
||||
return {
|
||||
user?.id?.trim() ?? "",
|
||||
user?.account?.trim() ?? "",
|
||||
user?.getID().trim() ?? "",
|
||||
}..removeWhere((item) => item.isEmpty);
|
||||
}
|
||||
|
||||
bool _userProfileMatchesAnyIdentity(
|
||||
SocialChatUserProfile? user,
|
||||
Set<String> identityKeys,
|
||||
) {
|
||||
if (identityKeys.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return _userProfileIdentityKeys(user).any(identityKeys.contains);
|
||||
}
|
||||
|
||||
void _rememberConfirmedSelfMicIdentity(SocialChatUserProfile? user) {
|
||||
_confirmedSelfMicIdentityKeys
|
||||
..clear()
|
||||
..addAll(_userProfileIdentityKeys(user));
|
||||
}
|
||||
|
||||
void _clearConfirmedSelfMicIdentity() {
|
||||
_confirmedSelfMicIdentityKeys.clear();
|
||||
}
|
||||
|
||||
///房间红包列表
|
||||
List<SCRoomRedPacketListRes> redPacketList = [];
|
||||
|
||||
@ -1331,8 +1271,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool _shouldSuppressCurrentUserOnSeat(num index, {MicRes? seat}) {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final releaseIndex = _pendingSelfMicReleaseIndex;
|
||||
@ -1342,7 +1283,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
final resolvedSeat = seat ?? roomWheatMap[index];
|
||||
return _userProfileMatchesAnyIdentity(resolvedSeat?.user, currentUserKeys);
|
||||
return (resolvedSeat?.user?.id ?? "").trim() == currentUserId;
|
||||
}
|
||||
|
||||
MicRes? micAtIndexForDisplay(num index) {
|
||||
@ -1369,8 +1310,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Map<num, MicRes> nextMap, {
|
||||
required Map<num, MicRes> previousMap,
|
||||
}) {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return nextMap;
|
||||
}
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
@ -1382,20 +1324,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
targetIndex != null && sourceIndex != null && guardUntilMs > nowMs;
|
||||
final selfSeatIndices =
|
||||
nextMap.entries
|
||||
.where(
|
||||
(entry) => _userProfileMatchesAnyIdentity(
|
||||
entry.value.user,
|
||||
currentUserKeys,
|
||||
),
|
||||
)
|
||||
.where((entry) => entry.value.user?.id == currentUserId)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (targetIndex != null &&
|
||||
_userProfileMatchesAnyIdentity(
|
||||
nextMap[targetIndex]?.user,
|
||||
currentUserKeys,
|
||||
)) {
|
||||
nextMap[targetIndex]?.user?.id == currentUserId) {
|
||||
_clearSelfMicSwitchGuard();
|
||||
_clearSelfMicGoUpGuard();
|
||||
return nextMap;
|
||||
@ -1413,7 +1347,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
previousMap: previousMap,
|
||||
selfSeatIndices: selfSeatIndices,
|
||||
targetIndex: goUpIndex,
|
||||
currentUserKeys: currentUserKeys,
|
||||
currentUserId: currentUserId,
|
||||
);
|
||||
if (stabilizedMap != null) {
|
||||
return stabilizedMap;
|
||||
@ -1439,7 +1373,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
previousMap: previousMap,
|
||||
selfSeatIndices: selfSeatIndices,
|
||||
targetIndex: resolvedTargetIndex,
|
||||
currentUserKeys: currentUserKeys,
|
||||
currentUserId: currentUserId,
|
||||
);
|
||||
if (stabilizedMap != null) {
|
||||
return stabilizedMap;
|
||||
@ -1466,7 +1400,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
final releaseSeat = nextMap[releaseIndex];
|
||||
if (!_userProfileMatchesAnyIdentity(releaseSeat?.user, currentUserKeys)) {
|
||||
if (releaseSeat?.user?.id != currentUserId) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
return nextMap;
|
||||
}
|
||||
@ -1481,22 +1415,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
required Map<num, MicRes> previousMap,
|
||||
required Iterable<num> selfSeatIndices,
|
||||
required num targetIndex,
|
||||
required Set<String> currentUserKeys,
|
||||
required String currentUserId,
|
||||
}) {
|
||||
final optimisticTargetSeat = previousMap[targetIndex];
|
||||
if (optimisticTargetSeat == null ||
|
||||
!_userProfileMatchesAnyIdentity(
|
||||
optimisticTargetSeat.user,
|
||||
currentUserKeys,
|
||||
)) {
|
||||
optimisticTargetSeat.user?.id != currentUserId) {
|
||||
return null;
|
||||
}
|
||||
final incomingTargetSeat = nextMap[targetIndex];
|
||||
if (incomingTargetSeat?.user != null &&
|
||||
!_userProfileMatchesAnyIdentity(
|
||||
incomingTargetSeat?.user,
|
||||
currentUserKeys,
|
||||
)) {
|
||||
incomingTargetSeat?.user?.id != currentUserId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1592,12 +1520,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
MicRes? _currentUserMicSeat() {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
for (final mic in roomWheatMap.values) {
|
||||
if (_userProfileMatchesAnyIdentity(mic.user, currentUserKeys)) {
|
||||
if ((mic.user?.id ?? "").trim() == currentUserId) {
|
||||
return mic;
|
||||
}
|
||||
}
|
||||
@ -1605,8 +1534,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _syncSelfMicRuntimeState() {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if ((currentUserId ?? "").isEmpty) {
|
||||
return;
|
||||
}
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
@ -2458,10 +2387,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
bool keepAudioPublishing = false,
|
||||
}) async {
|
||||
if (!_roomRtcJoined || _roomRtcEngineAdapter == null) {
|
||||
debugPrint(
|
||||
'[RoomRtc] switchToBroadcaster skipped joined=$_roomRtcJoined '
|
||||
'hasAdapter=${_roomRtcEngineAdapter != null}',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
@ -2470,7 +2395,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted),
|
||||
);
|
||||
if (!switched) {
|
||||
debugPrint('[RoomRtc] switchToBroadcaster failed adapter=false');
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return false;
|
||||
@ -2483,7 +2407,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return true;
|
||||
} catch (error) {
|
||||
debugPrint('[RoomRtc] switchToBroadcaster error=$error');
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
_resyncHeartbeatFromRoomRtcState();
|
||||
return false;
|
||||
@ -2551,14 +2474,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> setSelfMicMutedFromBottom(bool muted) async {
|
||||
_notifySelfMicUiState(muted: muted);
|
||||
isMic = muted;
|
||||
notifyListeners();
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
final seatIndex =
|
||||
currentUserId.isNotEmpty
|
||||
? userOnMaiInIndex(currentUserId)
|
||||
: (_currentUserMicSeat()?.micIndex ?? -1);
|
||||
final seatIndex = userOnMaiInIndex(currentUserId);
|
||||
if (seatIndex < 0) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
final seat = roomWheatMap[seatIndex];
|
||||
@ -2576,27 +2498,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleSelfMicMutedFromBottom() {
|
||||
return setSelfMicMutedFromBottom(!isMic);
|
||||
}
|
||||
|
||||
Future<void> handleSelfMicRemovedByRemote({
|
||||
bool refreshMicList = true,
|
||||
}) async {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
_notifySelfMicUiState(muted: true, notifyIfUnchanged: true);
|
||||
if (currentUserId.isEmpty) {
|
||||
await _switchRoomRtcToAudience();
|
||||
return;
|
||||
}
|
||||
|
||||
final currentSeatIndex =
|
||||
currentUserId.isNotEmpty
|
||||
? userOnMaiInIndex(currentUserId)
|
||||
: (_currentUserMicSeat()?.micIndex ?? -1);
|
||||
_setSelfMicMuted(true);
|
||||
final currentSeatIndex = userOnMaiInIndex(currentUserId);
|
||||
isMic = true;
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
||||
if (currentSeatIndex > -1) {
|
||||
@ -2604,11 +2517,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
} else {
|
||||
_clearSelfMicReleaseGuard();
|
||||
}
|
||||
_clearCurrentUserFromSeats();
|
||||
_clearUserFromSeats(currentUserId);
|
||||
_roomRtcRoleIsBroadcaster = false;
|
||||
_syncSelfMicRuntimeState();
|
||||
_clearConfirmedSelfMicIdentity();
|
||||
_notifySelfMicUiState(notifyIfUnchanged: true);
|
||||
notifyListeners();
|
||||
|
||||
await _switchRoomRtcToAudience();
|
||||
if (refreshMicList) {
|
||||
@ -4125,63 +4037,25 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_roomRocketGiftRefreshRoomId = resolvedRoomId;
|
||||
_roomRocketGiftRefreshDirty = true;
|
||||
if (_roomRocketGiftRefreshTimer != null || _roomRocketGiftRefreshInFlight) {
|
||||
return;
|
||||
}
|
||||
_roomRocketGiftRefreshTimer = Timer(
|
||||
_roomRocketGiftRefreshThrottleDelay,
|
||||
() => unawaited(_runRoomRocketGiftStatusRefresh()),
|
||||
);
|
||||
}
|
||||
|
||||
void _schedulePendingRoomRocketGiftStatusRefreshIfNeeded() {
|
||||
if (!_roomRocketGiftRefreshDirty || _roomRocketGiftRefreshTimer != null) {
|
||||
return;
|
||||
}
|
||||
final pendingRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim();
|
||||
final currentRoomId =
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (pendingRoomId.isEmpty || pendingRoomId != currentRoomId) {
|
||||
return;
|
||||
}
|
||||
_roomRocketGiftRefreshTimer = Timer(
|
||||
_roomRocketGiftRefreshThrottleDelay,
|
||||
() => unawaited(_runRoomRocketGiftStatusRefresh()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runRoomRocketGiftStatusRefresh() async {
|
||||
_roomRocketGiftRefreshTimer = null;
|
||||
if (_roomRocketGiftRefreshInFlight) {
|
||||
return;
|
||||
}
|
||||
final resolvedRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim();
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
_roomRocketGiftRefreshDirty = false;
|
||||
return;
|
||||
}
|
||||
_roomRocketGiftRefreshDirty = false;
|
||||
if (resolvedRoomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
_schedulePendingRoomRocketGiftStatusRefreshIfNeeded();
|
||||
return;
|
||||
}
|
||||
_roomRocketGiftRefreshInFlight = true;
|
||||
try {
|
||||
await refreshRoomRocketStatus(roomId: resolvedRoomId);
|
||||
} finally {
|
||||
_roomRocketGiftRefreshInFlight = false;
|
||||
_schedulePendingRoomRocketGiftStatusRefreshIfNeeded();
|
||||
_cancelRoomRocketGiftStatusRefresh();
|
||||
for (final delay in _roomRocketGiftRefreshDelays) {
|
||||
final timer = Timer(delay, () {
|
||||
_roomRocketGiftRefreshTimers.removeWhere((item) => !item.isActive);
|
||||
if (resolvedRoomId !=
|
||||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
|
||||
return;
|
||||
}
|
||||
unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId));
|
||||
});
|
||||
_roomRocketGiftRefreshTimers.add(timer);
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelRoomRocketGiftStatusRefresh() {
|
||||
_roomRocketGiftRefreshTimer?.cancel();
|
||||
_roomRocketGiftRefreshTimer = null;
|
||||
_roomRocketGiftRefreshRoomId = null;
|
||||
_roomRocketGiftRefreshDirty = false;
|
||||
for (final timer in _roomRocketGiftRefreshTimers) {
|
||||
timer.cancel();
|
||||
}
|
||||
_roomRocketGiftRefreshTimers.clear();
|
||||
}
|
||||
|
||||
void _scheduleRoomRocketEntryReadyFallback(String roomId) {
|
||||
@ -4425,8 +4299,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
SCGiftVapSvgaManager().play(
|
||||
animationUrl,
|
||||
priority: SCGiftVapSvgaManager.rocketLaunchEffectPriority,
|
||||
type: SCGiftVapSvgaManager.rocketLaunchEffectType,
|
||||
priority: SCGiftVapSvgaManager.entryEffectPriority + 100,
|
||||
type: SCGiftVapSvgaManager.entryEffectType,
|
||||
);
|
||||
}
|
||||
|
||||
@ -5133,7 +5007,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicReleaseGuard();
|
||||
_clearConfirmedSelfMicIdentity();
|
||||
_seatResizeOccupantPreserveUntilMs = null;
|
||||
roomIsMute = false;
|
||||
rtmProvider?.roomAllMsgList.clear();
|
||||
@ -5143,7 +5016,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomVisualEffectsEnabled = false;
|
||||
_voiceRoomRouteVisible = false;
|
||||
_isExitingCurrentVoiceRoomSession = false;
|
||||
_setSelfMicMuted(true);
|
||||
isMic = true;
|
||||
isMusicPlaying = false;
|
||||
if (clearPersistedRoomMarker) {
|
||||
DataPersistence.setLastTimeRoomId("");
|
||||
@ -5289,150 +5162,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
).catchError((_) {});
|
||||
}
|
||||
|
||||
void _clearCurrentUserFromSeats({num? exceptIndex}) {
|
||||
_clearUsersFromSeatsByIdentityKeys(
|
||||
_currentUserIdentityKeys(),
|
||||
exceptIndex: exceptIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _clearUsersFromSeatsByIdentityKeys(
|
||||
Set<String> lookupKeys, {
|
||||
num? exceptIndex,
|
||||
}) {
|
||||
lookupKeys.removeWhere((item) => item.trim().isEmpty);
|
||||
if (lookupKeys.isEmpty) {
|
||||
void _clearUserFromSeats(String? userId, {num? exceptIndex}) {
|
||||
final normalizedUserId = (userId ?? "").trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap.forEach((seatIndex, seat) {
|
||||
final seatUserKeys = _userProfileIdentityKeys(seat.user);
|
||||
if (!seatUserKeys.any(lookupKeys.contains) || seatIndex == exceptIndex) {
|
||||
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
});
|
||||
}
|
||||
|
||||
void _applySelfMicGoUpState({
|
||||
required SocialChatUserProfile myUser,
|
||||
required num targetIndex,
|
||||
required SCMicGoUpRes micGoUpRes,
|
||||
required bool selfMicMuted,
|
||||
}) {
|
||||
_rememberConfirmedSelfMicIdentity(myUser);
|
||||
final previousSelfSeatIndex = userOnMaiInIndex(myUser.id ?? "");
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
|
||||
_clearSelfMicReleaseGuard();
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
targetIndex: targetIndex,
|
||||
);
|
||||
} else {
|
||||
_clearSelfMicSwitchGuard();
|
||||
_startSelfMicGoUpGuard(targetIndex: targetIndex);
|
||||
}
|
||||
_clearCurrentUserFromSeats(exceptIndex: targetIndex);
|
||||
|
||||
final currentSeat = roomWheatMap[targetIndex];
|
||||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
roomWheatMap[targetIndex] =
|
||||
currentSeat?.copyWith(
|
||||
user: currentUser,
|
||||
micMute: micGoUpRes.micMute,
|
||||
micLock: micGoUpRes.micLock,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
) ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: targetIndex,
|
||||
micLock: micGoUpRes.micLock,
|
||||
micMute: micGoUpRes.micMute,
|
||||
user: currentUser,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
_setSelfMicMuted(selfMicMuted);
|
||||
_syncSelfMicRuntimeState();
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _resolveSelfMicGoUpUser({
|
||||
required SocialChatUserProfile? localUser,
|
||||
required SCMicGoUpRes micGoUpRes,
|
||||
}) {
|
||||
final responseUser = _profileFromMicGoUpUser(micGoUpRes.user);
|
||||
if (localUser == null) {
|
||||
return responseUser;
|
||||
}
|
||||
if (responseUser == null) {
|
||||
return localUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
}
|
||||
return localUser.copyWith(
|
||||
id: _firstNonBlankText([responseUser.id, localUser.id]),
|
||||
account: _firstNonBlankText([responseUser.account, localUser.account]),
|
||||
userAvatar: _firstNonBlankText([
|
||||
localUser.userAvatar,
|
||||
responseUser.userAvatar,
|
||||
]),
|
||||
userNickname: _firstNonBlankText([
|
||||
localUser.userNickname,
|
||||
responseUser.userNickname,
|
||||
]),
|
||||
roles: _firstNonBlankText([
|
||||
localUser.roles,
|
||||
responseUser.roles,
|
||||
currenRoom?.entrants?.roles,
|
||||
]),
|
||||
charmLevel: localUser.charmLevel ?? responseUser.charmLevel,
|
||||
heartbeatVal: localUser.heartbeatVal ?? responseUser.heartbeatVal,
|
||||
userSex: localUser.userSex ?? responseUser.userSex,
|
||||
);
|
||||
}
|
||||
|
||||
SocialChatUserProfile? _profileFromMicGoUpUser(User? user) {
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
final id = _firstNonBlankText([user.id]);
|
||||
final account = _firstNonBlankText([user.account]);
|
||||
if (id == null && account == null) {
|
||||
return null;
|
||||
}
|
||||
return SocialChatUserProfile(
|
||||
id: id,
|
||||
account: account,
|
||||
charmLevel: user.charmLevel,
|
||||
heartbeatVal: user.heartbeatVal,
|
||||
roles: _firstNonBlankText([user.roles, currenRoom?.entrants?.roles]),
|
||||
userAvatar: _firstNonBlankText([user.userAvatar]),
|
||||
userNickname: _firstNonBlankText([user.userNickname, user.account]),
|
||||
userSex: user.userSex,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void debugApplySelfMicGoUpResponse({
|
||||
SocialChatUserProfile? localUser,
|
||||
required num targetIndex,
|
||||
required SCMicGoUpRes micGoUpRes,
|
||||
}) {
|
||||
final selfUser = _resolveSelfMicGoUpUser(
|
||||
localUser: localUser,
|
||||
micGoUpRes: micGoUpRes,
|
||||
);
|
||||
if (selfUser == null) {
|
||||
return;
|
||||
}
|
||||
_applySelfMicGoUpState(
|
||||
myUser: selfUser,
|
||||
targetIndex: targetIndex,
|
||||
micGoUpRes: micGoUpRes,
|
||||
selfMicMuted: micGoUpRes.micMute ?? false,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _openRoomUserInfoCard(String? userId) {
|
||||
final normalizedUserId = (userId ?? '').trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
@ -5493,35 +5235,66 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||
final selfMicMuted = micGoUpRes.micMute ?? false;
|
||||
final selfUser = _resolveSelfMicGoUpUser(
|
||||
localUser: myUser,
|
||||
micGoUpRes: micGoUpRes,
|
||||
);
|
||||
if (selfUser != null) {
|
||||
_applySelfMicGoUpState(
|
||||
myUser: selfUser,
|
||||
targetIndex: targetIndex,
|
||||
micGoUpRes: micGoUpRes,
|
||||
selfMicMuted: selfMicMuted,
|
||||
);
|
||||
notifyListeners();
|
||||
} else {
|
||||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||||
}
|
||||
|
||||
final switched = await _switchRoomRtcToBroadcaster(
|
||||
muted: selfMicMuted,
|
||||
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
||||
);
|
||||
if (!switched) {
|
||||
await _switchRoomRtcToAudience();
|
||||
try {
|
||||
await SCChatRoomRepository().micGoDown(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
targetIndex,
|
||||
);
|
||||
} catch (downError) {}
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
||||
_clearUserFromSeats(myUser?.id);
|
||||
_syncSelfMicRuntimeState();
|
||||
notifyListeners();
|
||||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||||
_resetSelfMicGoUpFailureState();
|
||||
SCTts.show(
|
||||
'Microphone audio failed to start, please try switching microphone again',
|
||||
_handleSelfMicGoUpFailure(
|
||||
'Failed to put on the microphone, TRTC switch failed',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final previousSelfSeatIndex =
|
||||
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
if (myUser != null) {
|
||||
_clearSelfMicReleaseGuard();
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
targetIndex: targetIndex,
|
||||
);
|
||||
} else {
|
||||
_clearSelfMicSwitchGuard();
|
||||
_startSelfMicGoUpGuard(targetIndex: targetIndex);
|
||||
}
|
||||
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
||||
final currentSeat = roomWheatMap[targetIndex];
|
||||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
roomWheatMap[targetIndex] =
|
||||
currentSeat?.copyWith(
|
||||
user: currentUser,
|
||||
micMute: micGoUpRes.micMute,
|
||||
micLock: micGoUpRes.micLock,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
) ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: targetIndex,
|
||||
micLock: micGoUpRes.micLock,
|
||||
micMute: micGoUpRes.micMute,
|
||||
user: currentUser,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
}
|
||||
isMic = selfMicMuted;
|
||||
_syncSelfMicRuntimeState();
|
||||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
if (isFz()) {
|
||||
@ -5559,16 +5332,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
final currentUserSeatIndex =
|
||||
currentUserId.trim().isNotEmpty
|
||||
? userOnMaiInIndex(currentUserId)
|
||||
: (_currentUserMicSeat()?.micIndex ?? -1);
|
||||
if (_userProfileMatchesAnyIdentity(
|
||||
roomWheatMap[index]?.user,
|
||||
currentUserKeys,
|
||||
)) {
|
||||
_setSelfMicMuted(true);
|
||||
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
|
||||
if (roomWheatMap[index]?.user?.id == currentUserId) {
|
||||
isMic = true;
|
||||
}
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
||||
@ -5579,10 +5345,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
|
||||
_clearCurrentUserFromSeats();
|
||||
await _switchRoomRtcToAudience();
|
||||
_clearUserFromSeats(currentUserId);
|
||||
_syncSelfMicRuntimeState();
|
||||
_clearConfirmedSelfMicIdentity();
|
||||
_notifySelfMicUiState(notifyIfUnchanged: true);
|
||||
notifyListeners();
|
||||
requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: false,
|
||||
minInterval: const Duration(milliseconds: 350),
|
||||
@ -5652,15 +5418,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///自己是否在麦上
|
||||
bool isOnMai() {
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
if (currentUserKeys.isEmpty) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
for (final entry in roomWheatMap.entries) {
|
||||
if (_userProfileMatchesAnyIdentity(
|
||||
micAtIndexForDisplay(entry.key)?.user,
|
||||
currentUserKeys,
|
||||
)) {
|
||||
if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() ==
|
||||
currentUserId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -5669,39 +5434,25 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
///自己是否在指定的麦上
|
||||
bool isOnMaiInIndex(num index) {
|
||||
return _userProfileMatchesAnyIdentity(
|
||||
micAtIndexForDisplay(index)?.user,
|
||||
_currentUserIdentityKeys(),
|
||||
);
|
||||
return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() ==
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
}
|
||||
|
||||
///点击的用户在哪个麦上
|
||||
num userOnMaiInIndex(String userId) {
|
||||
return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true);
|
||||
}
|
||||
|
||||
num _findUserOnMaiInIndex(
|
||||
String userId, {
|
||||
required bool useDisplaySeatForCurrentUser,
|
||||
}) {
|
||||
final normalizedUserId = userId.trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return -1;
|
||||
}
|
||||
num index = -1;
|
||||
final currentUserKeys = _currentUserIdentityKeys();
|
||||
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
|
||||
final lookupKeys = <String>{normalizedUserId};
|
||||
if (isCurrentUserQuery) {
|
||||
lookupKeys.addAll(currentUserKeys);
|
||||
}
|
||||
lookupKeys.removeWhere((item) => item.isEmpty);
|
||||
roomWheatMap.forEach((k, value) {
|
||||
final candidateSeat =
|
||||
useDisplaySeatForCurrentUser && isCurrentUserQuery
|
||||
final visibleSeat =
|
||||
normalizedUserId ==
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
|
||||
.trim()
|
||||
? micAtIndexForDisplay(k)
|
||||
: value;
|
||||
if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) {
|
||||
if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) {
|
||||
index = k;
|
||||
}
|
||||
});
|
||||
@ -5992,10 +5743,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
GlobalKey<State<StatefulWidget>>? getSeatGlobalKeyByIndex(String userId) {
|
||||
///需要有人的座位
|
||||
num index = _findUserOnMaiInIndex(
|
||||
userId,
|
||||
useDisplaySeatForCurrentUser: false,
|
||||
);
|
||||
num index = userOnMaiInIndex(userId);
|
||||
if (index > -1) {
|
||||
return seatGlobalKeyMap[index];
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import 'package:yumi/shared/tools/sc_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
|
||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_chat_bubble_message_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_broadcast_assets.dart';
|
||||
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
|
||||
@ -41,7 +40,6 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_image_elem.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_extension.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_receipt.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_msg_create_info_result.dart';
|
||||
import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart';
|
||||
@ -278,33 +276,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
].join("|");
|
||||
}
|
||||
|
||||
bool _shouldNotifyFloatingGiftListener(Msg msg) {
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
return true;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
return msg.user != null && msg.gift != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _notifyFloatingGiftListenerFromAddMsg(Msg msg) {
|
||||
if (!_shouldNotifyFloatingGiftListener(msg)) {
|
||||
return;
|
||||
}
|
||||
_giftFxLog(
|
||||
'trigger floating gift listener from addMsg '
|
||||
'type=${msg.type} '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'sendUserId=${msg.user?.id} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'quantity=${msg.number} '
|
||||
'targetUserIds=${(msg.targetUserIds ?? const <String>[]).join(",")} '
|
||||
'hasListener=${msgFloatingGiftListener != null}',
|
||||
);
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
|
||||
String _firstNonBlank(Iterable<String?> values) {
|
||||
for (final value in values) {
|
||||
final text = value?.trim();
|
||||
@ -2645,18 +2616,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (createTextMessageRes.code == 0) {
|
||||
// 文本信息创建成功
|
||||
String id = createTextMessageRes.data!.id!;
|
||||
final chatBubble =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.getChatBox();
|
||||
final chatBubbleCloudCustomData = scEncodeChatBubbleCloudCustomData(
|
||||
chatBubble,
|
||||
);
|
||||
final chatBubbleSnapshotValue = scEncodeChatBubbleSnapshotValue(
|
||||
chatBubble,
|
||||
);
|
||||
// 发送文本消息
|
||||
// 在sendMessage时,若只填写receiver则发个人用户单聊消息
|
||||
// 若只填写groupID则发群组消息
|
||||
// 若填写了receiver与groupID则发群内的个人用户,消息在群聊中显示,只有指定receiver能看见
|
||||
String receiveId = toConversation.userID!;
|
||||
V2TimValueCallback<V2TimMessage> sendMessageRes = await TencentImSDKPlugin
|
||||
.v2TIMManager
|
||||
.getMessageManager()
|
||||
@ -2665,20 +2629,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
receiver: toConversation.userID!, // 接收人id
|
||||
needReadReceipt: true,
|
||||
groupID: '', // 是否需要已读回执
|
||||
isSupportMessageExtension: chatBubbleSnapshotValue != null,
|
||||
cloudCustomData: chatBubbleCloudCustomData,
|
||||
);
|
||||
if (sendMessageRes.code == 0) {
|
||||
// 发送成功
|
||||
_onNew1v1Message(sendMessageRes.data);
|
||||
if (chatBubbleSnapshotValue != null) {
|
||||
unawaited(
|
||||
_setC2CChatBubbleMessageExtension(
|
||||
sendMessageRes.data?.msgID,
|
||||
chatBubbleSnapshotValue,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
SCTts.show(
|
||||
'create fail,code:${sendMessageRes.code},${sendMessageRes.desc}',
|
||||
@ -2687,33 +2641,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setC2CChatBubbleMessageExtension(
|
||||
String? msgID,
|
||||
String snapshotValue,
|
||||
) async {
|
||||
final id = msgID?.trim();
|
||||
if (id == null || id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await TencentImSDKPlugin.v2TIMManager
|
||||
.getMessageManager()
|
||||
.setMessageExtensions(
|
||||
msgID: id,
|
||||
extensions: [
|
||||
V2TimMessageExtension(
|
||||
extensionKey: scChatBubbleMessageExtensionKey,
|
||||
extensionValue: snapshotValue,
|
||||
),
|
||||
],
|
||||
);
|
||||
} catch (error) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[chat_bubble] set extension failed msgID=$id error=$error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///发送单聊自定义消息
|
||||
Future<void> sendC2CCustomMsg(
|
||||
String msg,
|
||||
@ -3050,7 +2977,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (mergedGiftMsg != null) {
|
||||
msgAllListener?.call(mergedGiftMsg);
|
||||
msgGiftListener?.call(mergedGiftMsg);
|
||||
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
@ -3142,39 +3071,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomGiftMsgList.removeAt(roomGiftMsgList.length - 1);
|
||||
}
|
||||
msgGiftListener?.call(msg);
|
||||
_notifyFloatingGiftListenerFromAddMsg(msg);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _luckGiftTargetUserIdsFromMsg(Msg msg) {
|
||||
final targetUserIds = <String>[];
|
||||
void addTargetUserId(dynamic userId) {
|
||||
final normalizedUserId = userId?.toString().trim() ?? '';
|
||||
if (normalizedUserId.isEmpty ||
|
||||
targetUserIds.contains(normalizedUserId)) {
|
||||
return;
|
||||
if (msg.type == SCRoomMsgType.gift) {
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
}
|
||||
targetUserIds.add(normalizedUserId);
|
||||
}
|
||||
|
||||
for (final userId in msg.targetUserIds ?? const <String>[]) {
|
||||
addTargetUserId(userId);
|
||||
}
|
||||
|
||||
final rawTargetUserIds = (msg.msg ?? '').trim();
|
||||
if (rawTargetUserIds.startsWith('[')) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawTargetUserIds);
|
||||
if (decoded is List) {
|
||||
for (final userId in decoded) {
|
||||
addTargetUserId(userId);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
addTargetUserId(msg.toUser?.id);
|
||||
return targetUserIds;
|
||||
}
|
||||
|
||||
void _enqueueCpRelationBroadcastFloatingIfNeeded(
|
||||
@ -5149,6 +5049,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
RoomRocketLaunchBroadcastMessage launch,
|
||||
) {
|
||||
_clearRoomRocketLaunchCountdownForRoom(launch.roomId);
|
||||
_pendingRoomRocketRewardPopups.remove(launch.roomId);
|
||||
_recentRoomRocketRewardProbeTimes.remove(launch.roomId);
|
||||
_markRoomRocketLaunchEffectConsumed(launch);
|
||||
}
|
||||
|
||||
@ -6109,15 +6011,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
currentContext,
|
||||
listen: false,
|
||||
);
|
||||
if (_isCurrentVisibleVoiceRoom(launchRoomId)) {
|
||||
_markRoomRocketRewardProbe(launch.roomId);
|
||||
_playRoomRocketLaunchEffectsAndReward(launch);
|
||||
} else {
|
||||
rtcProvider.advanceRoomRocketStatusAfterLaunch(launch);
|
||||
rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh(
|
||||
roomId: launchRoomId,
|
||||
);
|
||||
}
|
||||
rtcProvider.advanceRoomRocketStatusAfterLaunch(launch);
|
||||
rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh(
|
||||
roomId: launchRoomId,
|
||||
);
|
||||
}
|
||||
}
|
||||
_scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId);
|
||||
@ -6593,7 +6490,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'giftPhoto=${msg.gift?.giftPhoto}',
|
||||
);
|
||||
} else {
|
||||
final targetUserIds = _luckGiftTargetUserIdsFromMsg(msg);
|
||||
final targetUserIds =
|
||||
(jsonDecode(msg.msg ?? "") as List)
|
||||
.map((e) => e as String)
|
||||
.toList();
|
||||
_giftFxLog(
|
||||
'recv LUCK_GIFT_ANIM_OTHER '
|
||||
'giftPhoto=${msg.gift?.giftPhoto} '
|
||||
@ -6608,7 +6508,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
targetUserIds,
|
||||
),
|
||||
);
|
||||
if (!_shouldNotifyFloatingGiftListener(msg)) {
|
||||
if (msg.user != null && msg.toUser != null && msg.gift != null) {
|
||||
_giftFxLog(
|
||||
'trigger floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
||||
'sendUserId=${msg.user?.id} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'quantity=${msg.number} '
|
||||
'giftId=${msg.gift?.id}',
|
||||
);
|
||||
msgFloatingGiftListener?.call(msg);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip floating gift listener from LUCK_GIFT_ANIM_OTHER '
|
||||
'reason=incomplete_msg '
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import 'package:yumi/app/routes/sc_routes.dart';
|
||||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||||
@ -105,60 +104,49 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
} else if (authType == SCAuthType.APPLE.name) {
|
||||
this.authType = authType;
|
||||
try {
|
||||
_authDebugLog('apple sign-in start');
|
||||
final isAvailable = await SignInWithApple.isAvailable();
|
||||
_authDebugLog('apple sign-in availability=$isAvailable');
|
||||
if (!isAvailable) {
|
||||
SCTts.show("Sign in with Apple is not available on this device.");
|
||||
return;
|
||||
}
|
||||
final appleCredential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName, // 请求姓名权限
|
||||
],
|
||||
);
|
||||
uid = appleCredential.userIdentifier ?? "";
|
||||
_authDebugLog(
|
||||
'apple credential success userIdentifier=${_maskForDebug(uid)} '
|
||||
'hasAuthorizationCode=${appleCredential.authorizationCode.isNotEmpty} '
|
||||
'hasIdentityToken=${(appleCredential.identityToken ?? "").isNotEmpty} '
|
||||
'hasEmail=${(appleCredential.email ?? "").isNotEmpty}',
|
||||
);
|
||||
if (uid.isEmpty) {
|
||||
SCTts.show("Apple authorization failed. Please try again.");
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.show();
|
||||
await DataPersistence.setPendingChannelAuth(authType, uid);
|
||||
var user = await SCAccountRepository().loginForChannel(authType, uid);
|
||||
if (!context.mounted) {
|
||||
if (uid.isNotEmpty) {
|
||||
await DataPersistence.setPendingChannelAuth(authType, uid);
|
||||
var user = await SCAccountRepository().loginForChannel(authType, uid);
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
final canContinue = await SCVersionUtils.checkReview(
|
||||
context: context,
|
||||
);
|
||||
if (!context.mounted || !canContinue) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
await Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).resetLocalRoomState(
|
||||
fallbackRtmProvider: Provider.of<RtmProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
),
|
||||
);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
notifyListeners();
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
}
|
||||
AccountStorage().setCurrentUser(user);
|
||||
await DataPersistence.clearPendingChannelAuth();
|
||||
if (!context.mounted) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
final canContinue = await SCVersionUtils.checkReview(context: context);
|
||||
if (!context.mounted || !canContinue) {
|
||||
SCLoadingManager.hide();
|
||||
return;
|
||||
}
|
||||
await Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).resetLocalRoomState(
|
||||
fallbackRtmProvider: Provider.of<RtmProvider>(context, listen: false),
|
||||
);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
SCLoadingManager.hide();
|
||||
notifyListeners();
|
||||
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
|
||||
} catch (e) {
|
||||
SCLoadingManager.hide();
|
||||
_showAuthError(e);
|
||||
@ -174,56 +162,13 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void _showAuthError(Object error) {
|
||||
_authDebugLog('authenticateUser error: $error');
|
||||
|
||||
if (error is DioException) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error is SignInWithAppleAuthorizationException) {
|
||||
_showAppleAuthorizationError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error is SignInWithAppleNotSupportedException) {
|
||||
SCTts.show("Sign in with Apple is not available on this device.");
|
||||
return;
|
||||
}
|
||||
|
||||
final message = error.toString().replaceFirst("Exception: ", "").trim();
|
||||
if (message.isNotEmpty) {
|
||||
SCTts.show(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAppleAuthorizationError(
|
||||
SignInWithAppleAuthorizationException error,
|
||||
) {
|
||||
if (error.code == AuthorizationErrorCode.canceled) {
|
||||
return;
|
||||
}
|
||||
if (error.code == AuthorizationErrorCode.unknown) {
|
||||
SCTts.show(
|
||||
"Apple sign-in is not available. Please check the app signing profile.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
SCTts.show("Apple authorization failed. Please try again.");
|
||||
}
|
||||
|
||||
void _authDebugLog(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[Auth][Apple] $message');
|
||||
}
|
||||
}
|
||||
|
||||
String _maskForDebug(String value) {
|
||||
if (value.isEmpty) {
|
||||
return '<empty>';
|
||||
}
|
||||
if (value.length <= 8) {
|
||||
return '***';
|
||||
}
|
||||
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,97 +468,14 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Map<String, List<Emojis>> emojiByTab = {};
|
||||
List<SCRoomEmojiRes> roomEmojiGroups = [];
|
||||
bool _isFetchingRoomEmojis = false;
|
||||
bool _hasRoomEmojisLoaded = false;
|
||||
bool _roomEmojisLoadedFromAll = false;
|
||||
|
||||
bool get isFetchingRoomEmojis => _isFetchingRoomEmojis;
|
||||
|
||||
bool get hasRoomEmojisLoaded => _hasRoomEmojisLoaded;
|
||||
|
||||
bool get roomEmojisLoadedFromAll => _roomEmojisLoadedFromAll;
|
||||
|
||||
bool get hasRemoteRoomEmojis =>
|
||||
roomEmojiGroups.any((group) => (group.emojis ?? const []).isNotEmpty);
|
||||
|
||||
///emoji表情
|
||||
Future<void> emojiAll({bool forceRefresh = false}) async {
|
||||
if (_isFetchingRoomEmojis) {
|
||||
_roomEmojiDebugLog('skip load: request already in flight');
|
||||
return;
|
||||
}
|
||||
if (_hasRoomEmojisLoaded && !forceRefresh && _roomEmojisLoadedFromAll) {
|
||||
_roomEmojiDebugLog(
|
||||
'skip load: cached groups=${roomEmojiGroups.length} '
|
||||
'tabs=${emojiByTab.length} source=/material/emoji/all',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_hasRoomEmojisLoaded && !forceRefresh && !_roomEmojisLoadedFromAll) {
|
||||
_roomEmojiDebugLog(
|
||||
'reload: cached data was not loaded from temporary /all source',
|
||||
);
|
||||
}
|
||||
|
||||
_isFetchingRoomEmojis = true;
|
||||
_roomEmojiDebugLog('load start forceRefresh=$forceRefresh');
|
||||
try {
|
||||
final roomEmojis = await SCChatRoomRepository().emojiAll();
|
||||
_roomEmojiDebugLog(
|
||||
'load response source=/material/emoji/all '
|
||||
'rawGroups=${roomEmojis.length} '
|
||||
'rawEmojiTotal=${_countRoomEmojiItems(roomEmojis)}',
|
||||
);
|
||||
roomEmojiGroups =
|
||||
roomEmojis
|
||||
.where(
|
||||
(group) =>
|
||||
group.have == true && (group.emojis ?? const []).isNotEmpty,
|
||||
)
|
||||
.toList();
|
||||
final filteredNoAccess =
|
||||
roomEmojis.where((group) => group.have != true).length;
|
||||
final filteredEmptyGroups =
|
||||
roomEmojis
|
||||
.where(
|
||||
(group) =>
|
||||
group.have == true && (group.emojis ?? const []).isEmpty,
|
||||
)
|
||||
.length;
|
||||
emojiByTab.clear();
|
||||
for (final value in roomEmojiGroups) {
|
||||
final groupName =
|
||||
value.groupName?.trim().isNotEmpty == true
|
||||
? value.groupName!.trim()
|
||||
: (value.groupCode ?? value.id ?? "");
|
||||
if (groupName.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
emojiByTab[groupName] = value.emojis ?? [];
|
||||
}
|
||||
_hasRoomEmojisLoaded = true;
|
||||
_roomEmojisLoadedFromAll = true;
|
||||
_roomEmojiDebugLog(
|
||||
'load success visibleGroups=${roomEmojiGroups.length} '
|
||||
'filteredNoAccess=$filteredNoAccess '
|
||||
'filteredEmptyGroups=$filteredEmptyGroups '
|
||||
'tabs=${emojiByTab.length} '
|
||||
'visibleEmojiTotal=${_countRoomEmojiItems(roomEmojiGroups)} '
|
||||
'sample=${_roomEmojiGroupSample(roomEmojiGroups)}',
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_roomEmojiDebugLog('load failed error=$error');
|
||||
debugPrintStack(
|
||||
label: '[RoomEmoji] load stack',
|
||||
stackTrace: stackTrace,
|
||||
maxFrames: 8,
|
||||
);
|
||||
return;
|
||||
} finally {
|
||||
_isFetchingRoomEmojis = false;
|
||||
notifyListeners();
|
||||
void emojiAll() async {
|
||||
var roomEmojis = await SCChatRoomRepository().emojiAll();
|
||||
for (var value in roomEmojis) {
|
||||
emojiByTab[value.groupName!] = value.emojis ?? [];
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
///加载等级资源
|
||||
@ -578,31 +495,3 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _roomEmojiDebugLog(String message) {
|
||||
debugPrint('[RoomEmoji] $message');
|
||||
}
|
||||
|
||||
int _countRoomEmojiItems(List<SCRoomEmojiRes> groups) {
|
||||
return groups.fold<int>(
|
||||
0,
|
||||
(total, group) => total + (group.emojis?.length ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
String _roomEmojiGroupSample(List<SCRoomEmojiRes> groups) {
|
||||
if (groups.isEmpty) {
|
||||
return '[]';
|
||||
}
|
||||
return groups
|
||||
.take(3)
|
||||
.map((group) {
|
||||
final emojis = group.emojis ?? const <Emojis>[];
|
||||
final first = emojis.isNotEmpty ? emojis.first : null;
|
||||
return '{id=${group.id}, code=${group.groupCode}, name=${group.groupName}, '
|
||||
'have=${group.have}, amount=${group.amount}, count=${emojis.length}, '
|
||||
'firstType=${first?.sourceType}, firstSource=${first?.sourceUrl}, '
|
||||
'firstCover=${first?.coverUrl}}';
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
|
||||
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
|
||||
|
||||
@ -346,7 +345,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 正常购买流依赖 pendingCompletePurchase;启动/回前台补偿流需要允许
|
||||
// 2. 正常购买流会依赖 pendingCompletePurchase;启动/回前台补偿流必须允许
|
||||
// 已确认但仍未消费的一次性商品再次上报后端,否则用户付款后退出 App 会漏单。
|
||||
if (!forceSubmit && !purchase.pendingCompletePurchase) {
|
||||
return;
|
||||
@ -368,7 +367,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
|
||||
// 4. 服务器验证
|
||||
await _submitGooglePayVerification(
|
||||
await SCConfigRepositoryImp().googlePay(
|
||||
purchase.productID,
|
||||
signature,
|
||||
purchaseData,
|
||||
@ -457,7 +456,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
final isUnacknowledged =
|
||||
purchase.pendingCompletePurchase ||
|
||||
!purchase.billingClientPurchase.isAcknowledged;
|
||||
// Google 只会返回未消费的一次性商品;即使它已确认,也说明仍可补上报后端。
|
||||
// Google 只会返回未消费的一次性商品;即使它已被确认,也说明仍可补上报后端。
|
||||
return isPurchased &&
|
||||
(isUnacknowledged || productType == 'consumable');
|
||||
}).toList();
|
||||
@ -477,38 +476,6 @@ class AndroidPaymentProcessor extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<SCGooglePayRes> _submitGooglePayVerification(
|
||||
String product,
|
||||
String signature,
|
||||
String purchaseData,
|
||||
) async {
|
||||
const api = 'order/purchase-pay/google';
|
||||
final paramsForLog = <String, dynamic>{
|
||||
'product': product,
|
||||
'signature': _clipForDebugLog(signature),
|
||||
'purchaseData': _clipForDebugLog(purchaseData),
|
||||
};
|
||||
if (kDebugMode) {
|
||||
debugPrint('[GooglePay] request api=$api params=$paramsForLog');
|
||||
}
|
||||
final result = await SCConfigRepositoryImp().googlePay(
|
||||
product,
|
||||
signature,
|
||||
purchaseData,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
debugPrint('[GooglePay] response api=$api result=${result.toJson()}');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String _clipForDebugLog(String value) {
|
||||
if (value.length <= 240) {
|
||||
return value;
|
||||
}
|
||||
return '${value.substring(0, 120)}...${value.substring(value.length - 80)}';
|
||||
}
|
||||
|
||||
// 交付商品
|
||||
Future<void> _deliverProduct(String productId) async {
|
||||
// 实现您的业务逻辑
|
||||
|
||||
@ -1,197 +1,146 @@
|
||||
// id : 0
|
||||
// sysOrigin : ""
|
||||
// groupCode : ""
|
||||
// groupName : ""
|
||||
// cover : ""
|
||||
// have : false
|
||||
// amount : 0.0
|
||||
// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
||||
|
||||
class SCRoomEmojiRes {
|
||||
SCRoomEmojiRes({
|
||||
String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,
|
||||
}) {
|
||||
_id = id;
|
||||
_sysOrigin = sysOrigin;
|
||||
_groupCode = groupCode;
|
||||
_groupName = groupName;
|
||||
_cover = cover;
|
||||
_have = have;
|
||||
_amount = amount;
|
||||
_emojis = emojis;
|
||||
}
|
||||
|
||||
SCRoomEmojiRes.fromJson(dynamic json) {
|
||||
final map = json is Map ? json : const <String, dynamic>{};
|
||||
_id = _stringOrNull(map['id']);
|
||||
_sysOrigin = _stringOrNull(map['sysOrigin']);
|
||||
_groupCode = _stringOrNull(map['groupCode']);
|
||||
_groupName = _stringOrNull(map['groupName']);
|
||||
_cover = _stringOrNull(map['cover']);
|
||||
_have = _boolOrNull(map['have']);
|
||||
_amount = _numOrNull(map['amount']);
|
||||
if (map['emojis'] is List) {
|
||||
_emojis = [];
|
||||
map['emojis'].forEach((v) {
|
||||
_emojis?.add(Emojis.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String? _id;
|
||||
String? _sysOrigin;
|
||||
String? _groupCode;
|
||||
String? _groupName;
|
||||
String? _cover;
|
||||
bool? _have;
|
||||
num? _amount;
|
||||
List<Emojis>? _emojis;
|
||||
SCRoomEmojiRes copyWith({
|
||||
String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,
|
||||
}) => SCRoomEmojiRes(
|
||||
id: id ?? _id,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
groupCode: groupCode ?? _groupCode,
|
||||
groupName: groupName ?? _groupName,
|
||||
cover: cover ?? _cover,
|
||||
have: have ?? _have,
|
||||
amount: amount ?? _amount,
|
||||
emojis: emojis ?? _emojis,
|
||||
);
|
||||
|
||||
String? get id => _id;
|
||||
String? get sysOrigin => _sysOrigin;
|
||||
String? get groupCode => _groupCode;
|
||||
String? get groupName => _groupName;
|
||||
String? get cover => _cover;
|
||||
bool? get have => _have;
|
||||
num? get amount => _amount;
|
||||
List<Emojis>? get emojis => _emojis;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
map['groupCode'] = _groupCode;
|
||||
map['groupName'] = _groupName;
|
||||
map['cover'] = _cover;
|
||||
map['have'] = _have;
|
||||
map['amount'] = _amount;
|
||||
if (_emojis != null) {
|
||||
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
/// sourceUrl : ""
|
||||
/// sourceType : 0
|
||||
/// coverUrl : ""
|
||||
/// type : ""
|
||||
|
||||
class Emojis {
|
||||
Emojis({String? sourceUrl, num? sourceType, String? coverUrl, String? type}) {
|
||||
_sourceUrl = sourceUrl;
|
||||
_sourceType = sourceType;
|
||||
_coverUrl = coverUrl;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
Emojis.fromJson(dynamic json) {
|
||||
final map = json is Map ? json : const <String, dynamic>{};
|
||||
_sourceUrl = _stringOrNull(map['sourceUrl']);
|
||||
_sourceType = _numOrNull(map['sourceType']);
|
||||
_coverUrl = _stringOrNull(map['coverUrl']);
|
||||
_type = _stringOrNull(map['type']);
|
||||
}
|
||||
|
||||
String? _sourceUrl;
|
||||
num? _sourceType;
|
||||
String? _coverUrl;
|
||||
String? _type;
|
||||
Emojis copyWith({
|
||||
String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,
|
||||
}) => Emojis(
|
||||
sourceUrl: sourceUrl ?? _sourceUrl,
|
||||
sourceType: sourceType ?? _sourceType,
|
||||
coverUrl: coverUrl ?? _coverUrl,
|
||||
type: type ?? _type,
|
||||
);
|
||||
|
||||
String? get sourceUrl => _sourceUrl;
|
||||
num? get sourceType => _sourceType;
|
||||
String? get coverUrl => _coverUrl;
|
||||
String? get type => _type;
|
||||
String get previewUrl {
|
||||
final cover = _coverUrl?.trim() ?? "";
|
||||
if (cover.isNotEmpty) {
|
||||
return cover;
|
||||
}
|
||||
return _sourceUrl?.trim() ?? "";
|
||||
}
|
||||
|
||||
String get sendUrl => _sourceUrl?.trim() ?? previewUrl;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['sourceUrl'] = _sourceUrl;
|
||||
map['sourceType'] = _sourceType;
|
||||
map['coverUrl'] = _coverUrl;
|
||||
map['type'] = _type;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
String? _stringOrNull(dynamic value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final text = value.toString();
|
||||
return text.isEmpty ? null : text;
|
||||
}
|
||||
|
||||
num? _numOrNull(dynamic value) {
|
||||
if (value is num) {
|
||||
return value;
|
||||
}
|
||||
if (value is String) {
|
||||
return num.tryParse(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool? _boolOrNull(dynamic value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
if (value is num) {
|
||||
return value != 0;
|
||||
}
|
||||
if (value is String) {
|
||||
final normalized = value.trim().toLowerCase();
|
||||
if (normalized == 'true' || normalized == '1') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == '0') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/// id : 0
|
||||
/// sysOrigin : ""
|
||||
/// groupCode : ""
|
||||
/// groupName : ""
|
||||
/// cover : ""
|
||||
/// have : false
|
||||
/// amount : 0.0
|
||||
/// emojis : [{"sourceUrl":"","sourceType":0,"coverUrl":"","type":""}]
|
||||
|
||||
class SCRoomEmojiRes {
|
||||
SCRoomEmojiRes({
|
||||
String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,}){
|
||||
_id = id;
|
||||
_sysOrigin = sysOrigin;
|
||||
_groupCode = groupCode;
|
||||
_groupName = groupName;
|
||||
_cover = cover;
|
||||
_have = have;
|
||||
_amount = amount;
|
||||
_emojis = emojis;
|
||||
}
|
||||
|
||||
SCRoomEmojiRes.fromJson(dynamic json) {
|
||||
_id = json['id'];
|
||||
_sysOrigin = json['sysOrigin'];
|
||||
_groupCode = json['groupCode'];
|
||||
_groupName = json['groupName'];
|
||||
_cover = json['cover'];
|
||||
_have = json['have'];
|
||||
_amount = json['amount'];
|
||||
if (json['emojis'] != null) {
|
||||
_emojis = [];
|
||||
json['emojis'].forEach((v) {
|
||||
_emojis?.add(Emojis.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
String? _id;
|
||||
String? _sysOrigin;
|
||||
String? _groupCode;
|
||||
String? _groupName;
|
||||
String? _cover;
|
||||
bool? _have;
|
||||
num? _amount;
|
||||
List<Emojis>? _emojis;
|
||||
SCRoomEmojiRes copyWith({ String? id,
|
||||
String? sysOrigin,
|
||||
String? groupCode,
|
||||
String? groupName,
|
||||
String? cover,
|
||||
bool? have,
|
||||
num? amount,
|
||||
List<Emojis>? emojis,
|
||||
}) => SCRoomEmojiRes( id: id ?? _id,
|
||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||
groupCode: groupCode ?? _groupCode,
|
||||
groupName: groupName ?? _groupName,
|
||||
cover: cover ?? _cover,
|
||||
have: have ?? _have,
|
||||
amount: amount ?? _amount,
|
||||
emojis: emojis ?? _emojis,
|
||||
);
|
||||
String? get id => _id;
|
||||
String? get sysOrigin => _sysOrigin;
|
||||
String? get groupCode => _groupCode;
|
||||
String? get groupName => _groupName;
|
||||
String? get cover => _cover;
|
||||
bool? get have => _have;
|
||||
num? get amount => _amount;
|
||||
List<Emojis>? get emojis => _emojis;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['id'] = _id;
|
||||
map['sysOrigin'] = _sysOrigin;
|
||||
map['groupCode'] = _groupCode;
|
||||
map['groupName'] = _groupName;
|
||||
map['cover'] = _cover;
|
||||
map['have'] = _have;
|
||||
map['amount'] = _amount;
|
||||
if (_emojis != null) {
|
||||
map['emojis'] = _emojis?.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// sourceUrl : ""
|
||||
/// sourceType : 0
|
||||
/// coverUrl : ""
|
||||
/// type : ""
|
||||
|
||||
class Emojis {
|
||||
Emojis({
|
||||
String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,}){
|
||||
_sourceUrl = sourceUrl;
|
||||
_sourceType = sourceType;
|
||||
_coverUrl = coverUrl;
|
||||
_type = type;
|
||||
}
|
||||
|
||||
Emojis.fromJson(dynamic json) {
|
||||
_sourceUrl = json['sourceUrl'];
|
||||
_sourceType = json['sourceType'];
|
||||
_coverUrl = json['coverUrl'];
|
||||
_type = json['type'];
|
||||
}
|
||||
String? _sourceUrl;
|
||||
num? _sourceType;
|
||||
String? _coverUrl;
|
||||
String? _type;
|
||||
Emojis copyWith({ String? sourceUrl,
|
||||
num? sourceType,
|
||||
String? coverUrl,
|
||||
String? type,
|
||||
}) => Emojis( sourceUrl: sourceUrl ?? _sourceUrl,
|
||||
sourceType: sourceType ?? _sourceType,
|
||||
coverUrl: coverUrl ?? _coverUrl,
|
||||
type: type ?? _type,
|
||||
);
|
||||
String? get sourceUrl => _sourceUrl;
|
||||
num? get sourceType => _sourceType;
|
||||
String? get coverUrl => _coverUrl;
|
||||
String? get type => _type;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['sourceUrl'] = _sourceUrl;
|
||||
map['sourceType'] = _sourceType;
|
||||
map['coverUrl'] = _coverUrl;
|
||||
map['type'] = _type;
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@ -131,12 +131,6 @@ abstract class SocialChatRoomRepository {
|
||||
///麦位表情
|
||||
Future<List<SCRoomEmojiRes>> emojiAll();
|
||||
|
||||
///当前用户已拥有的麦位表情
|
||||
Future<List<SCRoomEmojiRes>> emojiOwned();
|
||||
|
||||
///购买表情包
|
||||
Future<bool> purchaseEmoji(String id);
|
||||
|
||||
///设置房间用户身份
|
||||
Future<bool> changeRoomRole(
|
||||
String roomId,
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -71,38 +70,20 @@ class MediaCache {
|
||||
bool ignoreCache = false,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_debug('getFile start ignoreCache=$ignoreCache url=$url');
|
||||
try {
|
||||
if (ignoreCache) {
|
||||
// 强制重新下载
|
||||
final fileInfo = await _cacheManager.downloadFile(
|
||||
url,
|
||||
authHeaders: headers,
|
||||
);
|
||||
_debug(
|
||||
'getFile success bytes=${await fileInfo.file.length()} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds} path=${fileInfo.file.path}',
|
||||
);
|
||||
return fileInfo.file;
|
||||
} else {
|
||||
// 优先从缓存获取
|
||||
final cacheFile = await _cacheManager.getSingleFile(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
_debug(
|
||||
'getFile success bytes=${await cacheFile.length()} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds} path=${cacheFile.path}',
|
||||
);
|
||||
return cacheFile;
|
||||
}
|
||||
} catch (error) {
|
||||
_debug(
|
||||
'getFile failed elapsedMs=${stopwatch.elapsedMilliseconds} '
|
||||
'url=$url error=$error',
|
||||
if (ignoreCache) {
|
||||
// 强制重新下载
|
||||
final fileInfo = await _cacheManager.downloadFile(
|
||||
url,
|
||||
authHeaders: headers,
|
||||
);
|
||||
rethrow;
|
||||
return fileInfo.file;
|
||||
} else {
|
||||
// 优先从缓存获取
|
||||
final cacheFile = await _cacheManager.getSingleFile(
|
||||
url,
|
||||
headers: headers,
|
||||
);
|
||||
return cacheFile;
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,10 +134,3 @@ class MediaCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _debug(String message) {
|
||||
if (!kDebugMode) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[FileCacheManager] $message');
|
||||
}
|
||||
|
||||
@ -85,23 +85,16 @@ class OverlayManager {
|
||||
SCFloatingMessage message, {
|
||||
required String debugLabel,
|
||||
}) {
|
||||
void enqueueNow() {
|
||||
if (_isDisposed || _isSuppressed) {
|
||||
return;
|
||||
}
|
||||
unawaited(_warmMessageImages(message));
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
}
|
||||
|
||||
if (message.type == 1) {
|
||||
enqueueNow();
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(_warmMessageImages(message));
|
||||
SCRoomEffectScheduler().scheduleDeferredEffect(
|
||||
debugLabel: debugLabel,
|
||||
action: enqueueNow,
|
||||
action: () {
|
||||
if (_isDisposed || _isSuppressed) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -322,82 +322,37 @@ class SCConfigRepositoryImp implements SocialChatConfigRepository {
|
||||
///sys/version/manage/release/latest
|
||||
@override
|
||||
Future<SCVersionManageLatestRes> versionManageLatest() async {
|
||||
const apiName = 'releaseLatest';
|
||||
const path = '/sys/version/manage/release/latest';
|
||||
const queryParams = <String, dynamic>{};
|
||||
_debugVersionRequest(apiName, path, queryParams);
|
||||
try {
|
||||
final result = await http.get<SCVersionManageLatestRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
|
||||
queryParams: queryParams,
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse(apiName, path, json);
|
||||
return SCVersionManageLatestRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
_debugVersionError(apiName, path, queryParams, error);
|
||||
rethrow;
|
||||
}
|
||||
final result = await http.get<SCVersionManageLatestRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e84ac21c90fcd0966a13d2ce9e8845eb8e580afbe66f9f0fef79429cd5c1e0687",
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse('releaseLatest', json);
|
||||
return SCVersionManageLatestRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
///sys/version/manage/latest/review
|
||||
@override
|
||||
Future<VersionManageLatesReviewRes> versionManageLatestReview() async {
|
||||
const apiName = 'latestReview';
|
||||
const path = '/sys/version/manage/latest/review';
|
||||
const queryParams = <String, dynamic>{};
|
||||
_debugVersionRequest(apiName, path, queryParams);
|
||||
try {
|
||||
final result = await http.get<VersionManageLatesReviewRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
|
||||
queryParams: queryParams,
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse(apiName, path, json);
|
||||
return VersionManageLatesReviewRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
_debugVersionError(apiName, path, queryParams, error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
void _debugVersionRequest(
|
||||
String name,
|
||||
String path,
|
||||
Map<String, dynamic> queryParams,
|
||||
) {
|
||||
debugPrint(
|
||||
'[SCVersionUtils][API] $name request path=$path params=$queryParams',
|
||||
final result = await http.get<VersionManageLatesReviewRes>(
|
||||
"ee9584f714ded864780e47dab2cf4a2e11ce42bdd061186d4efe3305b73f10fe574aff257ce7e668d08f4caccd1c6232",
|
||||
fromJson: (json) {
|
||||
_debugVersionResponse('latestReview', json);
|
||||
return VersionManageLatesReviewRes.fromJson(json);
|
||||
},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _debugVersionResponse(String name, String path, dynamic json) {
|
||||
void _debugVersionResponse(String name, dynamic json) {
|
||||
try {
|
||||
debugPrint(
|
||||
'[SCVersionUtils][API] $name response path=$path rawBody=${jsonEncode(json)}',
|
||||
);
|
||||
debugPrint('[SCVersionUtils] $name rawBody=${jsonEncode(json)}');
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'[SCVersionUtils][API] $name response path=$path rawBody=$json encodeError=$e',
|
||||
);
|
||||
debugPrint('[SCVersionUtils] $name rawBody=$json encodeError=$e');
|
||||
}
|
||||
}
|
||||
|
||||
void _debugVersionError(
|
||||
String name,
|
||||
String path,
|
||||
Map<String, dynamic> queryParams,
|
||||
Object error,
|
||||
) {
|
||||
debugPrint(
|
||||
'[SCVersionUtils][API] $name failed path=$path params=$queryParams error=$error',
|
||||
);
|
||||
}
|
||||
|
||||
///sys/config/customer-service
|
||||
@override
|
||||
Future<SocialChatUserProfile> customerService() async {
|
||||
|
||||
@ -55,45 +55,6 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
debugPrint('[GiftFx][repo] $message');
|
||||
}
|
||||
|
||||
void _roomEmojiRepoDebugLog(String message) {
|
||||
debugPrint('[RoomEmoji][repo] $message');
|
||||
}
|
||||
|
||||
void _roomEmojiRequestLog(
|
||||
String method,
|
||||
String path, {
|
||||
dynamic query,
|
||||
dynamic data,
|
||||
}) {
|
||||
_roomEmojiRepoDebugLog(
|
||||
'request method=$method path=$path '
|
||||
'query=${_roomEmojiDebugJson(query ?? const <String, dynamic>{})} '
|
||||
'body=${_roomEmojiDebugJson(data)}',
|
||||
);
|
||||
}
|
||||
|
||||
void _roomEmojiResponseLog(String method, String path, dynamic responseBody) {
|
||||
_roomEmojiRepoDebugLog(
|
||||
'response method=$method path=$path '
|
||||
'responseBody(unwrapped)=${_roomEmojiDebugJson(responseBody)}',
|
||||
);
|
||||
}
|
||||
|
||||
String _roomEmojiDebugJson(dynamic data) {
|
||||
return _clipGiftDebug(_stringifyGiftDebug(data));
|
||||
}
|
||||
|
||||
int _roomEmojiTotal(List<SCRoomEmojiRes> groups) {
|
||||
return groups.fold<int>(
|
||||
0,
|
||||
(total, group) => total + (group.emojis?.length ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
int _ownedEmojiGroups(List<SCRoomEmojiRes> groups) {
|
||||
return groups.where((group) => group.have == true).length;
|
||||
}
|
||||
|
||||
String _giftRepoErrorDetails(Object error) {
|
||||
if (error is DioException) {
|
||||
final request = error.requestOptions;
|
||||
@ -807,87 +768,13 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
||||
///material/emoji/all
|
||||
@override
|
||||
Future<List<SCRoomEmojiRes>> emojiAll() async {
|
||||
const method = 'GET';
|
||||
const path = '/material/emoji/all';
|
||||
_roomEmojiRequestLog(method, path);
|
||||
try {
|
||||
final result = await http.get<List<SCRoomEmojiRes>>(
|
||||
"1d2546ea50639e65f04d029dd0a9655eda4aeb3a4bd182b663d60d87cb3f2938",
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) {
|
||||
_roomEmojiResponseLog(method, path, json);
|
||||
final list = json is List ? json : const [];
|
||||
_roomEmojiRepoDebugLog(
|
||||
'parse method=$method path=$path bodyType=${json.runtimeType} '
|
||||
'groups=${list.length}',
|
||||
);
|
||||
return list.map((e) => SCRoomEmojiRes.fromJson(e)).toList();
|
||||
},
|
||||
);
|
||||
_roomEmojiRepoDebugLog(
|
||||
'success method=$method path=$path groups=${result.length} '
|
||||
'emojiTotal=${_roomEmojiTotal(result)} ownedGroups=${_ownedEmojiGroups(result)}',
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
_roomEmojiRepoDebugLog(
|
||||
'failed method=$method path=$path error=${_giftRepoErrorDetails(error)}',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
///material/emoji
|
||||
@override
|
||||
Future<List<SCRoomEmojiRes>> emojiOwned() async {
|
||||
const method = 'GET';
|
||||
const path = '/material/emoji';
|
||||
_roomEmojiRequestLog(method, path);
|
||||
try {
|
||||
final result = await http.get<List<SCRoomEmojiRes>>(
|
||||
path,
|
||||
extra: const {BaseNetworkClient.silentErrorToastKey: true},
|
||||
fromJson: (json) {
|
||||
_roomEmojiResponseLog(method, path, json);
|
||||
final list = json is List ? json : const [];
|
||||
_roomEmojiRepoDebugLog(
|
||||
'parse method=$method path=$path bodyType=${json.runtimeType} '
|
||||
'groups=${list.length}',
|
||||
);
|
||||
return list.map((e) => SCRoomEmojiRes.fromJson(e)).toList();
|
||||
},
|
||||
);
|
||||
_roomEmojiRepoDebugLog(
|
||||
'success method=$method path=$path groups=${result.length} '
|
||||
'emojiTotal=${_roomEmojiTotal(result)}',
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
_roomEmojiRepoDebugLog(
|
||||
'failed method=$method path=$path error=${_giftRepoErrorDetails(error)}',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
///material/emoji/purchase
|
||||
@override
|
||||
Future<bool> purchaseEmoji(String id) async {
|
||||
const method = 'POST';
|
||||
const path = '/material/emoji/purchase';
|
||||
final params = {"id": id};
|
||||
_roomEmojiRequestLog(method, path, data: params);
|
||||
await http.post<Object?>(
|
||||
path,
|
||||
data: params,
|
||||
allowNullBody: true,
|
||||
fromJson: (json) {
|
||||
_roomEmojiResponseLog(method, path, json);
|
||||
return json;
|
||||
},
|
||||
final result = await http.get<List<SCRoomEmojiRes>>(
|
||||
"1d2546ea50639e65f04d029dd0a9655eda4aeb3a4bd182b663d60d87cb3f2938",
|
||||
fromJson:
|
||||
(json) =>
|
||||
(json as List).map((e) => SCRoomEmojiRes.fromJson(e)).toList(),
|
||||
);
|
||||
_roomEmojiRepoDebugLog('success method=$method path=$path result=true');
|
||||
return true;
|
||||
return result;
|
||||
}
|
||||
|
||||
///room/member/change-role
|
||||
|
||||
@ -72,45 +72,6 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
}
|
||||
}
|
||||
|
||||
void _authDebugLog(String message) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[Auth][API] $message');
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _sanitizeAuthPayload(Map<String, dynamic> payload) {
|
||||
return payload.map((key, value) {
|
||||
final lowerKey = key.toLowerCase();
|
||||
if (lowerKey.contains('openid') ||
|
||||
lowerKey.contains('token') ||
|
||||
lowerKey.contains('sign') ||
|
||||
lowerKey.contains('pwd')) {
|
||||
return MapEntry(key, _maskForDebug(value?.toString() ?? ''));
|
||||
}
|
||||
return MapEntry(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
String _maskForDebug(String value) {
|
||||
if (value.isEmpty) {
|
||||
return '<empty>';
|
||||
}
|
||||
if (value.length <= 8) {
|
||||
return '***';
|
||||
}
|
||||
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
|
||||
}
|
||||
|
||||
String _loginResultSummary(SocialChatLoginRes result) {
|
||||
return jsonEncode({
|
||||
'hasToken': (result.token ?? '').isNotEmpty,
|
||||
'hasUserSig': (result.userSig ?? '').isNotEmpty,
|
||||
'userId': result.userProfile?.id,
|
||||
'account': result.userProfile?.account,
|
||||
'nickname': result.userProfile?.userNickname,
|
||||
});
|
||||
}
|
||||
|
||||
String _cpPairSummary(CPRes pair) {
|
||||
return '{relationType=${pair.relationType}, status=${pair.status}, '
|
||||
'me=${pair.meUserId}/${pair.meAccount}, '
|
||||
@ -237,17 +198,11 @@ class SCAccountRepository implements SocialChatUserRepository {
|
||||
) async {
|
||||
final data = <String, dynamic>{"authType": authType, "openId": openId};
|
||||
data.addAll(await SCMobileLoginContext.loginBodyFields());
|
||||
_authDebugLog(
|
||||
'request POST /auth/account/login/channel params=${jsonEncode(_sanitizeAuthPayload(data))}',
|
||||
);
|
||||
final result = await http.post<SocialChatLoginRes>(
|
||||
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
|
||||
data: data,
|
||||
fromJson: (json) => SocialChatLoginRes.fromJson(json),
|
||||
);
|
||||
_authDebugLog(
|
||||
'response POST /auth/account/login/channel result=${_loginResultSummary(result)}',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
|
||||
const String scChatBubbleCloudDataKey = 'scChatBubble';
|
||||
const String scChatBubbleMessageExtensionKey = scChatBubbleCloudDataKey;
|
||||
|
||||
String? scEncodeChatBubbleCloudCustomData(PropsResources? chatBubble) {
|
||||
final snapshot = scChatBubbleSnapshotJson(chatBubble);
|
||||
if (snapshot == null) {
|
||||
return null;
|
||||
}
|
||||
return jsonEncode(<String, dynamic>{scChatBubbleCloudDataKey: snapshot});
|
||||
}
|
||||
|
||||
String? scEncodeChatBubbleSnapshotValue(PropsResources? chatBubble) {
|
||||
final snapshot = scChatBubbleSnapshotJson(chatBubble);
|
||||
return snapshot == null ? null : jsonEncode(snapshot);
|
||||
}
|
||||
|
||||
PropsResources? scDecodeChatBubbleFromCloudCustomData(String? cloudCustomData) {
|
||||
final raw = cloudCustomData?.trim();
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
return null;
|
||||
}
|
||||
final data = decoded[scChatBubbleCloudDataKey];
|
||||
if (data is Map) {
|
||||
return PropsResources.fromJson(data);
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
PropsResources? scDecodeChatBubbleSnapshotValue(String? snapshotValue) {
|
||||
final raw = snapshotValue?.trim();
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map) {
|
||||
return PropsResources.fromJson(decoded);
|
||||
}
|
||||
} catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic>? scChatBubbleSnapshotJson(PropsResources? resource) {
|
||||
if (!_hasChatBubbleImage(resource)) {
|
||||
return null;
|
||||
}
|
||||
final map = <String, dynamic>{};
|
||||
void put(String key, String? value) {
|
||||
final text = value?.trim();
|
||||
if (text != null && text.isNotEmpty) {
|
||||
map[key] = text;
|
||||
}
|
||||
}
|
||||
|
||||
put('id', resource?.id);
|
||||
put('name', resource?.name);
|
||||
put('type', resource?.type);
|
||||
put('cover', resource?.cover);
|
||||
put('expand', resource?.expand);
|
||||
put('sourceUrl', resource?.sourceUrl);
|
||||
put('imSendCoverUrl', resource?.imSendCoverUrl);
|
||||
put('imOpenedUrl', resource?.imOpenedUrl);
|
||||
put('imOpenedUrlTwo', resource?.imOpenedUrlTwo);
|
||||
put('imNotOpenedUrl', resource?.imNotOpenedUrl);
|
||||
put('roomSendCoverUrl', resource?.roomSendCoverUrl);
|
||||
put('roomOpenedUrl', resource?.roomOpenedUrl);
|
||||
put('roomNotOpenedUrl', resource?.roomNotOpenedUrl);
|
||||
return map;
|
||||
}
|
||||
|
||||
bool _hasChatBubbleImage(PropsResources? resource) {
|
||||
if (resource == null) {
|
||||
return false;
|
||||
}
|
||||
return <String?>[
|
||||
resource.imSendCoverUrl,
|
||||
resource.imOpenedUrl,
|
||||
resource.imOpenedUrlTwo,
|
||||
resource.imNotOpenedUrl,
|
||||
resource.roomSendCoverUrl,
|
||||
resource.roomOpenedUrl,
|
||||
resource.roomNotOpenedUrl,
|
||||
resource.cover,
|
||||
resource.expand,
|
||||
resource.sourceUrl,
|
||||
].any((value) => (value?.trim() ?? '').isNotEmpty);
|
||||
}
|
||||
@ -91,13 +91,11 @@
|
||||
// if (_isDisposed) return;
|
||||
// if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
||||
// MovieEntity entity;
|
||||
// final manager = SCGiftVapSvgaManager();
|
||||
// final cached = manager.getCachedSvgaEntity(task.path);
|
||||
// if (cached != null) {
|
||||
// entity = cached;
|
||||
// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) {
|
||||
// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!;
|
||||
// } else {
|
||||
// entity = await SVGAParser.shared.decodeFromAssets(task.path);
|
||||
// manager.cacheSvgaEntity(task.path, entity);
|
||||
// SCGiftVapSvgaManager().videoItemCache[task.path] = entity;
|
||||
// }
|
||||
// _roomGiftsvgaController?.videoItem = entity;
|
||||
// _roomGiftsvgaController?.reset();
|
||||
@ -118,16 +116,14 @@
|
||||
// if (_isDisposed) return;
|
||||
// if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
||||
// MovieEntity? entity;
|
||||
// final manager = SCGiftVapSvgaManager();
|
||||
// final cached = manager.getCachedSvgaEntity(task.path);
|
||||
// if (cached != null) {
|
||||
// entity = cached;
|
||||
// if (SCGiftVapSvgaManager().videoItemCache.containsKey(task.path)) {
|
||||
// entity = SCGiftVapSvgaManager().videoItemCache[task.path]!;
|
||||
// entity.autorelease = false;
|
||||
// } else {
|
||||
// try {
|
||||
// entity = await SVGAParser.shared.decodeFromURL(task.path);
|
||||
// entity.autorelease = false;
|
||||
// manager.cacheSvgaEntity(task.path, entity);
|
||||
// SCGiftVapSvgaManager().videoItemCache[task.path] = entity;
|
||||
// } catch (e) {
|
||||
// _isPlaying = false;
|
||||
// }
|
||||
|
||||
@ -92,8 +92,6 @@ class SCEntryPopupCoordinator {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
var popupTapped = false;
|
||||
final popupTapCompleter = Completer<void>();
|
||||
_log('show key=${popup.key} popupId=${popup.popupId}');
|
||||
await SCEntryPopupDialog.show(
|
||||
context,
|
||||
@ -103,22 +101,9 @@ class SCEntryPopupCoordinator {
|
||||
unawaited(_markShown(popup));
|
||||
},
|
||||
onTap: () {
|
||||
if (popupTapped) {
|
||||
return;
|
||||
}
|
||||
popupTapped = true;
|
||||
unawaited(
|
||||
_handlePopupTap(context, popup, onSelectRootTab).whenComplete(() {
|
||||
if (!popupTapCompleter.isCompleted) {
|
||||
popupTapCompleter.complete();
|
||||
}
|
||||
}),
|
||||
);
|
||||
unawaited(_handlePopupTap(context, popup, onSelectRootTab));
|
||||
},
|
||||
);
|
||||
if (popupTapped) {
|
||||
await popupTapCompleter.future;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_log('skip reason=no-image-ready-candidates');
|
||||
|
||||
@ -16,23 +16,19 @@ import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||
class SCGiftVapSvgaManager {
|
||||
static const int giftEffectType = 0;
|
||||
static const int entryEffectType = 1;
|
||||
static const int rocketLaunchEffectType = 2;
|
||||
static const int giftEffectPriority = 100;
|
||||
static const int entryEffectPriority = 1000;
|
||||
static const int rocketLaunchEffectPriority = 2000;
|
||||
static const int _maxPendingGiftTaskCount = 24;
|
||||
static const int _maxPendingEntryTaskCount = 5;
|
||||
static const int _maxPendingRocketLaunchTaskCount = 3;
|
||||
static const int _maxConsecutiveEntryTasksBeforeGift = 2;
|
||||
static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
|
||||
static const Duration _entryTaskTtl = Duration(seconds: 6);
|
||||
final Map<String, MovieEntity> _videoItemCache = {};
|
||||
Map<String, MovieEntity> videoItemCache = {};
|
||||
static SCGiftVapSvgaManager? _inst;
|
||||
static const int _maxPreloadConcurrency = 1;
|
||||
static const int _maxPreloadQueueLength = 12;
|
||||
static const int _maxSvgaCacheEntries = 8;
|
||||
static const int _maxPlayablePathCacheEntries = 24;
|
||||
final Map<String, int> _svgaRetainCounts = {};
|
||||
|
||||
SCGiftVapSvgaManager._internal();
|
||||
|
||||
@ -44,10 +40,6 @@ class SCGiftVapSvgaManager {
|
||||
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
|
||||
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
||||
);
|
||||
final SCPriorityQueue<SCVapTask> _rocketLaunchQueue =
|
||||
SCPriorityQueue<SCVapTask>(
|
||||
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
||||
);
|
||||
SCGiftMp4Controller? _mp4Controller;
|
||||
SVGAAnimationController? _rsc;
|
||||
bool _play = false;
|
||||
@ -105,11 +97,6 @@ class SCGiftVapSvgaManager {
|
||||
if (type == entryEffectType) {
|
||||
return priority > entryEffectPriority ? priority : entryEffectPriority;
|
||||
}
|
||||
if (type == rocketLaunchEffectType) {
|
||||
return priority > rocketLaunchEffectPriority
|
||||
? priority
|
||||
: rocketLaunchEffectPriority;
|
||||
}
|
||||
if (priority != 0) {
|
||||
return priority;
|
||||
}
|
||||
@ -118,15 +105,10 @@ class SCGiftVapSvgaManager {
|
||||
|
||||
bool _isEntryTask(SCVapTask task) => task.type == entryEffectType;
|
||||
|
||||
bool _isRocketLaunchTask(SCVapTask task) =>
|
||||
task.type == rocketLaunchEffectType;
|
||||
|
||||
int get _pendingTaskCount =>
|
||||
_giftQueue.length + _entryQueue.length + _rocketLaunchQueue.length;
|
||||
int get _pendingTaskCount => _giftQueue.length + _entryQueue.length;
|
||||
|
||||
String get _queueSummary =>
|
||||
'rocket=${_rocketLaunchQueue.length} entry=${_entryQueue.length} '
|
||||
'gift=${_giftQueue.length}';
|
||||
'entry=${_entryQueue.length} gift=${_giftQueue.length}';
|
||||
|
||||
bool _isControllerReady(SCVapTask task) {
|
||||
if (_needsSvgaController(task.path)) {
|
||||
@ -145,7 +127,7 @@ class SCGiftVapSvgaManager {
|
||||
|
||||
bool _isPreloadedOrLoading(String path) {
|
||||
if (_needsSvgaController(path)) {
|
||||
return _videoItemCache.containsKey(path) ||
|
||||
return videoItemCache.containsKey(path) ||
|
||||
_svgaLoadTasks.containsKey(path);
|
||||
}
|
||||
final pathType = SCPathUtils.getPathType(path);
|
||||
@ -222,19 +204,7 @@ class SCGiftVapSvgaManager {
|
||||
await _loadSvgaEntity(path);
|
||||
return;
|
||||
}
|
||||
final pathType = SCPathUtils.getPathType(path);
|
||||
if (pathType == PathType.asset) {
|
||||
await SCGiftMp4Controller.preloadVapLayout(
|
||||
path: path,
|
||||
sourceType: SCGiftMp4SourceType.asset,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final playablePath = await _ensurePlayableFilePath(path);
|
||||
await SCGiftMp4Controller.preloadVapLayout(
|
||||
path: playablePath,
|
||||
sourceType: SCGiftMp4SourceType.file,
|
||||
);
|
||||
await _ensurePlayableFilePath(path);
|
||||
}
|
||||
|
||||
Future<MovieEntity> _loadSvgaEntity(String path) async {
|
||||
@ -303,80 +273,20 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
}
|
||||
|
||||
MovieEntity? getCachedSvgaEntity(String path, {bool retain = false}) {
|
||||
final normalizedPath = path.trim();
|
||||
if (normalizedPath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final entity = _touchCachedSvgaEntity(normalizedPath);
|
||||
if (entity != null && retain) {
|
||||
_retainCachedSvgaEntity(normalizedPath);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
void cacheSvgaEntity(String path, MovieEntity entity, {bool retain = false}) {
|
||||
final normalizedPath = path.trim();
|
||||
if (normalizedPath.isEmpty) {
|
||||
_disposeSvgaEntityIfUnused(entity);
|
||||
return;
|
||||
}
|
||||
_cacheSvgaEntity(normalizedPath, entity);
|
||||
if (retain) {
|
||||
_retainCachedSvgaEntity(normalizedPath);
|
||||
}
|
||||
}
|
||||
|
||||
MovieEntity? removeCachedSvgaEntity(String path, {bool dispose = false}) {
|
||||
final normalizedPath = path.trim();
|
||||
if (normalizedPath.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final entity = _videoItemCache.remove(normalizedPath);
|
||||
if (dispose &&
|
||||
entity != null &&
|
||||
!_isCachedSvgaEntityInUse(normalizedPath, entity)) {
|
||||
_disposeSvgaEntityIfUnused(entity);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
void releaseCachedSvgaEntity(String path) {
|
||||
final normalizedPath = path.trim();
|
||||
if (normalizedPath.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final count = _svgaRetainCounts[normalizedPath] ?? 0;
|
||||
if (count <= 1) {
|
||||
_svgaRetainCounts.remove(normalizedPath);
|
||||
_trimSvgaCache();
|
||||
return;
|
||||
}
|
||||
_svgaRetainCounts[normalizedPath] = count - 1;
|
||||
}
|
||||
|
||||
int get cachedSvgaEntityCount => _videoItemCache.length;
|
||||
|
||||
void _retainCachedSvgaEntity(String path) {
|
||||
_svgaRetainCounts[path] = (_svgaRetainCounts[path] ?? 0) + 1;
|
||||
}
|
||||
|
||||
MovieEntity? _touchCachedSvgaEntity(String path) {
|
||||
final cached = _videoItemCache.remove(path);
|
||||
final cached = videoItemCache.remove(path);
|
||||
if (cached != null) {
|
||||
_videoItemCache[path] = cached;
|
||||
videoItemCache[path] = cached;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
void _cacheSvgaEntity(String path, MovieEntity entity) {
|
||||
final previous = _videoItemCache.remove(path);
|
||||
if (previous != null &&
|
||||
!identical(previous, entity) &&
|
||||
!_isCachedSvgaEntityInUse(path, previous)) {
|
||||
final previous = videoItemCache.remove(path);
|
||||
if (previous != null && !identical(previous, entity)) {
|
||||
_disposeSvgaEntityIfUnused(previous);
|
||||
}
|
||||
_videoItemCache[path] = entity;
|
||||
videoItemCache[path] = entity;
|
||||
_trimSvgaCache();
|
||||
}
|
||||
|
||||
@ -395,19 +305,17 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
void _trimSvgaCache() {
|
||||
while (_videoItemCache.length > _maxSvgaCacheEntries) {
|
||||
MapEntry<String, MovieEntity>? removableEntry;
|
||||
for (final entry in _videoItemCache.entries) {
|
||||
if (!_isCachedSvgaEntityInUse(entry.key, entry.value)) {
|
||||
removableEntry = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (removableEntry == null) {
|
||||
while (videoItemCache.length > _maxSvgaCacheEntries) {
|
||||
final removableEntry = videoItemCache.entries.firstWhere(
|
||||
(entry) => !_isSvgaEntityInUse(entry.value),
|
||||
orElse: () => videoItemCache.entries.first,
|
||||
);
|
||||
if (_isSvgaEntityInUse(removableEntry.value)) {
|
||||
videoItemCache.remove(removableEntry.key);
|
||||
videoItemCache[removableEntry.key] = removableEntry.value;
|
||||
break;
|
||||
}
|
||||
_svgaRetainCounts.remove(removableEntry.key);
|
||||
_videoItemCache.remove(removableEntry.key);
|
||||
videoItemCache.remove(removableEntry.key);
|
||||
_disposeSvgaEntity(removableEntry.value);
|
||||
}
|
||||
}
|
||||
@ -420,11 +328,6 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
void _enqueueTask(SCVapTask task) {
|
||||
if (_isRocketLaunchTask(task)) {
|
||||
_rocketLaunchQueue.add(task);
|
||||
_trimRocketLaunchQueue();
|
||||
return;
|
||||
}
|
||||
if (_isEntryTask(task)) {
|
||||
_dropExpiredEntryTasks();
|
||||
_entryQueue.add(task);
|
||||
@ -435,27 +338,6 @@ class SCGiftVapSvgaManager {
|
||||
_trimGiftQueue();
|
||||
}
|
||||
|
||||
void _trimRocketLaunchQueue() {
|
||||
while (_rocketLaunchQueue.length > _maxPendingRocketLaunchTaskCount) {
|
||||
final removedTask = _removeRocketLaunchOverflowTask();
|
||||
_completeDroppedQueuedTask(removedTask, 'trim rocket launch queue');
|
||||
}
|
||||
}
|
||||
|
||||
SCVapTask _removeRocketLaunchOverflowTask() {
|
||||
final tasks = _rocketLaunchQueue.unorderedElements.cast<SCVapTask>();
|
||||
var candidate = tasks.first;
|
||||
for (final task in tasks.skip(1)) {
|
||||
if (task.priority < candidate.priority ||
|
||||
(task.priority == candidate.priority &&
|
||||
task.createdAt.isBefore(candidate.createdAt))) {
|
||||
candidate = task;
|
||||
}
|
||||
}
|
||||
_rocketLaunchQueue.remove(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
void _trimEntryQueue() {
|
||||
while (_entryQueue.length > _maxPendingEntryTaskCount) {
|
||||
final removedTask = _removeEntryOverflowTask();
|
||||
@ -512,9 +394,6 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
SCVapTask? _peekNextQueuedTask() {
|
||||
if (_rocketLaunchQueue.isNotEmpty) {
|
||||
return _rocketLaunchQueue.first;
|
||||
}
|
||||
_dropExpiredEntryTasks();
|
||||
if (_entryQueue.isNotEmpty) {
|
||||
final shouldYieldToGift =
|
||||
@ -529,11 +408,6 @@ class SCGiftVapSvgaManager {
|
||||
}
|
||||
|
||||
void _removeQueuedTask(SCVapTask task) {
|
||||
if (_isRocketLaunchTask(task)) {
|
||||
_rocketLaunchQueue.remove(task);
|
||||
_consecutiveEntryTaskCount = 0;
|
||||
return;
|
||||
}
|
||||
if (_isEntryTask(task)) {
|
||||
_entryQueue.remove(task);
|
||||
_consecutiveEntryTaskCount++;
|
||||
@ -881,7 +755,6 @@ class SCGiftVapSvgaManager {
|
||||
_cancelCurrentTaskWatchdog();
|
||||
_giftQueue.clear();
|
||||
_entryQueue.clear();
|
||||
_rocketLaunchQueue.clear();
|
||||
_preloadQueue.clear();
|
||||
_queuedPreloadPaths.clear();
|
||||
_activePreloadCount = 0;
|
||||
@ -896,15 +769,13 @@ class SCGiftVapSvgaManager {
|
||||
_preloadQueue.clear();
|
||||
_queuedPreloadPaths.clear();
|
||||
_activePreloadCount = 0;
|
||||
final entries = _videoItemCache.entries.toList(growable: false);
|
||||
final entries = videoItemCache.entries.toList(growable: false);
|
||||
for (final entry in entries) {
|
||||
final entity = entry.value;
|
||||
if (_isCachedSvgaEntityInUse(entry.key, entity) ||
|
||||
(!includeCurrent && _isSvgaEntityInUse(entity))) {
|
||||
if (!includeCurrent && _isSvgaEntityInUse(entity)) {
|
||||
continue;
|
||||
}
|
||||
_svgaRetainCounts.remove(entry.key);
|
||||
_videoItemCache.remove(entry.key);
|
||||
videoItemCache.remove(entry.key);
|
||||
_disposeSvgaEntity(entity);
|
||||
}
|
||||
_playablePathCache.clear();
|
||||
@ -914,10 +785,6 @@ class SCGiftVapSvgaManager {
|
||||
return identical(_rsc?.videoItem, entity);
|
||||
}
|
||||
|
||||
bool _isCachedSvgaEntityInUse(String path, MovieEntity entity) {
|
||||
return _isSvgaEntityInUse(entity) || (_svgaRetainCounts[path] ?? 0) > 0;
|
||||
}
|
||||
|
||||
void _disposeSvgaEntityIfUnused(MovieEntity entity) {
|
||||
if (_isSvgaEntityInUse(entity)) {
|
||||
return;
|
||||
|
||||
@ -186,7 +186,7 @@ showBottomInBottomDialog(
|
||||
Color? barrierColor,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
return showGeneralDialog(
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierLabel: '',
|
||||
barrierDismissible: barrierDismissible,
|
||||
|
||||
@ -29,17 +29,10 @@ class SCVersionUtils {
|
||||
'review=${_versionInfoDebug(versionInfo.review)} '
|
||||
'latest=${_versionInfoDebug(versionInfo.latest)}',
|
||||
);
|
||||
final review = _selectCurrentPlatformVersion(
|
||||
versionInfo.review,
|
||||
source: 'latestReview.review',
|
||||
);
|
||||
if (versionInfo.review != null) {
|
||||
SCGlobalConfig.isReview =
|
||||
review != null && "${review.version}" == SCGlobalConfig.version;
|
||||
_debugLog(
|
||||
'checkReview isReview=${SCGlobalConfig.isReview} '
|
||||
'selectedReview=${_versionInfoDebug(review)}',
|
||||
);
|
||||
"${versionInfo.review?.version}" == SCGlobalConfig.version;
|
||||
_debugLog('checkReview isReview=${SCGlobalConfig.isReview}');
|
||||
}
|
||||
final latest = await _resolveLatestVersion(versionInfo.latest);
|
||||
final shouldForceUpdate = latest != null && _shouldForceUpdate(latest);
|
||||
@ -103,56 +96,20 @@ class SCVersionUtils {
|
||||
static Future<SCVersionManageLatestRes?> _resolveLatestVersion(
|
||||
SCVersionManageLatestRes? latest,
|
||||
) async {
|
||||
final selectedLatest = _selectCurrentPlatformVersion(
|
||||
latest,
|
||||
source: 'latestReview.latest',
|
||||
);
|
||||
if (selectedLatest != null) {
|
||||
return selectedLatest;
|
||||
if (!_isVersionInfoEmpty(latest)) {
|
||||
return latest;
|
||||
}
|
||||
_debugLog(
|
||||
'checkReview latestReview latest unavailable for current platform, '
|
||||
'fallback releaseLatest',
|
||||
);
|
||||
_debugLog('checkReview latestReview latest empty, fallback releaseLatest');
|
||||
try {
|
||||
final fallback = await SCConfigRepositoryImp().versionManageLatest();
|
||||
_debugLog('checkReview fallbackLatest=${_versionInfoDebug(fallback)}');
|
||||
return _selectCurrentPlatformVersion(fallback, source: 'releaseLatest');
|
||||
return _isVersionInfoEmpty(fallback) ? latest : fallback;
|
||||
} catch (e) {
|
||||
_debugLog('checkReview fallback releaseLatest failed error=$e');
|
||||
return null;
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
static SCVersionManageLatestRes? _selectCurrentPlatformVersion(
|
||||
SCVersionManageLatestRes? version, {
|
||||
required String source,
|
||||
}) {
|
||||
if (_isVersionInfoEmpty(version)) {
|
||||
_debugLog('checkReview ignore $source reason=empty');
|
||||
return null;
|
||||
}
|
||||
final currentVersion = version!;
|
||||
if (_isCurrentPlatformVersion(currentVersion)) {
|
||||
_debugLog(
|
||||
'checkReview select $source currentPlatform=${_currentPlatformName()} '
|
||||
'version=${currentVersion.version} '
|
||||
'buildVersion=${currentVersion.buildVersion} '
|
||||
'downloadUrl=${currentVersion.downloadUrl}',
|
||||
);
|
||||
return currentVersion;
|
||||
}
|
||||
_debugLog(
|
||||
'checkReview ignore $source reason=platform_mismatch '
|
||||
'currentPlatform=${_currentPlatformName()} '
|
||||
'responsePlatform=${currentVersion.platform} '
|
||||
'version=${currentVersion.version} '
|
||||
'buildVersion=${currentVersion.buildVersion} '
|
||||
'downloadUrl=${currentVersion.downloadUrl}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _isVersionInfoEmpty(SCVersionManageLatestRes? version) {
|
||||
if (version == null) {
|
||||
return true;
|
||||
@ -188,25 +145,6 @@ class SCVersionUtils {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool _isCurrentPlatformVersion(SCVersionManageLatestRes version) {
|
||||
return _normalizePlatform(version.platform) == _currentPlatformKey();
|
||||
}
|
||||
|
||||
static String _currentPlatformName() {
|
||||
return Platform.isIOS ? 'iOS' : 'Android';
|
||||
}
|
||||
|
||||
static String _currentPlatformKey() {
|
||||
return Platform.isIOS ? 'ios' : 'android';
|
||||
}
|
||||
|
||||
static String _normalizePlatform(String? platform) {
|
||||
return (platform ?? '').trim().toLowerCase().replaceAll(
|
||||
RegExp(r'[^a-z0-9]'),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
static List<int> _versionParts(String? version) {
|
||||
final normalized = version?.trim() ?? "";
|
||||
if (normalized.isEmpty) {
|
||||
@ -339,14 +277,6 @@ class SCVersionUtils {
|
||||
|
||||
static Future<void> _openUpdateUrl(SCVersionManageLatestRes latest) async {
|
||||
final url = _resolveDownloadUrl(latest);
|
||||
final urlSource =
|
||||
(latest.downloadUrl ?? "").trim().isNotEmpty
|
||||
? 'api_downloadUrl'
|
||||
: (Platform.isIOS ? 'local_appStore' : 'local_googlePlay');
|
||||
_debugLog(
|
||||
'openUpdateUrl currentPlatform=${_currentPlatformName()} '
|
||||
'responsePlatform=${latest.platform} source=$urlSource url=$url',
|
||||
);
|
||||
if (url.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
@ -20,9 +19,6 @@ int? _resolveMemCacheDimension(double? logicalSize) {
|
||||
}
|
||||
|
||||
class CustomCachedImage extends StatelessWidget {
|
||||
static final Set<String> _loggedLoadingUrls = <String>{};
|
||||
static final Set<String> _loggedErrorUrls = <String>{};
|
||||
|
||||
final String imageUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
@ -30,7 +26,6 @@ class CustomCachedImage extends StatelessWidget {
|
||||
final double borderRadius;
|
||||
final Widget? placeholder;
|
||||
final Widget? errorWidget;
|
||||
final String? debugName;
|
||||
|
||||
const CustomCachedImage({
|
||||
super.key,
|
||||
@ -41,7 +36,6 @@ class CustomCachedImage extends StatelessWidget {
|
||||
this.borderRadius = 0,
|
||||
this.placeholder,
|
||||
this.errorWidget,
|
||||
this.debugName,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -57,34 +51,13 @@ class CustomCachedImage extends StatelessWidget {
|
||||
memCacheWidth: _resolveMemCacheDimension(width),
|
||||
memCacheHeight: _resolveMemCacheDimension(height),
|
||||
fit: fit,
|
||||
placeholder: (context, url) {
|
||||
_debugLoading(normalizedUrl);
|
||||
return placeholder ?? _defaultPlaceholder();
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
_debugError(normalizedUrl, error);
|
||||
return errorWidget ?? _defaultErrorWidget();
|
||||
},
|
||||
placeholder: (context, url) => placeholder ?? _defaultPlaceholder(),
|
||||
errorWidget:
|
||||
(context, url, error) => errorWidget ?? _defaultErrorWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _debugLoading(String url) {
|
||||
if (!kDebugMode || !_loggedLoadingUrls.add(url)) {
|
||||
return;
|
||||
}
|
||||
final label = debugName == null ? '' : '[$debugName] ';
|
||||
debugPrint('[NetworkImage][custom] ${label}loading url=$url');
|
||||
}
|
||||
|
||||
void _debugError(String url, Object error) {
|
||||
if (!kDebugMode || !_loggedErrorUrls.add('$url|$error')) {
|
||||
return;
|
||||
}
|
||||
final label = debugName == null ? '' : '[$debugName] ';
|
||||
debugPrint('[NetworkImage][custom] ${label}failed url=$url error=$error');
|
||||
}
|
||||
|
||||
Widget _defaultPlaceholder() {
|
||||
return const SCRotatingDotsLoading();
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
bool _isLoading = true;
|
||||
bool _isNetworkResource = false;
|
||||
bool _hasError = false;
|
||||
String? _retainedResource;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -85,15 +84,8 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
try {
|
||||
// 检查缓存
|
||||
MovieEntity? videoItem;
|
||||
final manager = SCGiftVapSvgaManager();
|
||||
final shouldRetain = _retainedResource != widget.resource;
|
||||
var retained = false;
|
||||
if (widget.useCache) {
|
||||
videoItem = manager.getCachedSvgaEntity(
|
||||
widget.resource,
|
||||
retain: shouldRetain,
|
||||
);
|
||||
retained = videoItem != null && shouldRetain;
|
||||
videoItem = SCGiftVapSvgaManager().videoItemCache[widget.resource];
|
||||
}
|
||||
|
||||
// 如果没有缓存,则进行解析
|
||||
@ -105,23 +97,11 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
videoItem.autorelease = false;
|
||||
// 存入缓存
|
||||
if (widget.useCache) {
|
||||
if (!mounted) {
|
||||
videoItem.dispose();
|
||||
return;
|
||||
}
|
||||
manager.cacheSvgaEntity(
|
||||
widget.resource,
|
||||
videoItem,
|
||||
retain: shouldRetain,
|
||||
);
|
||||
retained = shouldRetain;
|
||||
SCGiftVapSvgaManager().videoItemCache[widget.resource] = videoItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (retained) {
|
||||
_replaceRetainedResource(widget.resource);
|
||||
}
|
||||
setState(() {
|
||||
_animationController?.videoItem = videoItem;
|
||||
_isLoading = false;
|
||||
@ -184,34 +164,13 @@ class _SVGAHeadwearWidgetState extends State<SVGAHeadwearWidget>
|
||||
void reload() {
|
||||
if (widget.useCache) {
|
||||
// 清除缓存项
|
||||
SCGiftVapSvgaManager().removeCachedSvgaEntity(widget.resource);
|
||||
SCGiftVapSvgaManager().videoItemCache.remove(widget.resource);
|
||||
}
|
||||
_loadAnimation();
|
||||
}
|
||||
|
||||
void _replaceRetainedResource(String resource) {
|
||||
final previous = _retainedResource;
|
||||
if (previous == resource) {
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous);
|
||||
}
|
||||
_retainedResource = resource;
|
||||
}
|
||||
|
||||
void _releaseRetainedResource() {
|
||||
final resource = _retainedResource;
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_releaseRetainedResource();
|
||||
stop();
|
||||
_animationController?.dispose();
|
||||
super.dispose();
|
||||
|
||||
@ -270,7 +270,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
bool _isLoading = false;
|
||||
bool _hasError = false;
|
||||
String? _currentResource;
|
||||
String? _retainedResource;
|
||||
|
||||
// 用于存储SVGAImage组件的key,以便刷新占位符
|
||||
GlobalKey _svgaKey = GlobalKey();
|
||||
@ -355,13 +354,9 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
try {
|
||||
final isNetworkResource = resource.startsWith('http');
|
||||
MovieEntity? videoItem;
|
||||
final manager = SCGiftVapSvgaManager();
|
||||
final shouldRetain = _retainedResource != resource;
|
||||
var retained = false;
|
||||
|
||||
if (widget.useCache) {
|
||||
videoItem = manager.getCachedSvgaEntity(resource, retain: shouldRetain);
|
||||
retained = videoItem != null && shouldRetain;
|
||||
videoItem = SCGiftVapSvgaManager().videoItemCache[resource];
|
||||
}
|
||||
|
||||
if (videoItem == null) {
|
||||
@ -372,19 +367,11 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
videoItem.autorelease = false;
|
||||
|
||||
if (widget.useCache) {
|
||||
if (!mounted) {
|
||||
videoItem.dispose();
|
||||
return;
|
||||
}
|
||||
manager.cacheSvgaEntity(resource, videoItem, retain: shouldRetain);
|
||||
retained = shouldRetain;
|
||||
SCGiftVapSvgaManager().videoItemCache[resource] = videoItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
if (retained) {
|
||||
_replaceRetainedResource(resource);
|
||||
}
|
||||
// 应用动态数据(文本、图片替换)
|
||||
await _applyDynamicData(videoItem, dynamicData);
|
||||
|
||||
@ -566,7 +553,7 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
void reload() {
|
||||
if (_currentResource != null) {
|
||||
if (widget.useCache) {
|
||||
SCGiftVapSvgaManager().removeCachedSvgaEntity(_currentResource!);
|
||||
SCGiftVapSvgaManager().videoItemCache.remove(_currentResource);
|
||||
}
|
||||
|
||||
_loadAnimation(
|
||||
@ -579,7 +566,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_releaseRetainedResource();
|
||||
stop();
|
||||
_animationController?.dispose();
|
||||
// 从队列管理器中移除当前播放器
|
||||
@ -587,26 +573,6 @@ class _RoomEntranceWidgetState extends State<RoomEntranceWidget>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _replaceRetainedResource(String resource) {
|
||||
final previous = _retainedResource;
|
||||
if (previous == resource) {
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous);
|
||||
}
|
||||
_retainedResource = resource;
|
||||
}
|
||||
|
||||
void _releaseRetainedResource() {
|
||||
final resource = _retainedResource;
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 显示加载状态
|
||||
|
||||
@ -5,10 +5,6 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
|
||||
void _seatFlightLog(String message) {
|
||||
debugPrint('[GiftFx][seatFlight] $message');
|
||||
}
|
||||
|
||||
typedef RoomGiftSeatTargetResolver =
|
||||
GlobalKey<State<StatefulWidget>>? Function(String targetUserId);
|
||||
|
||||
@ -56,19 +52,8 @@ class RoomGiftSeatFlightController {
|
||||
if (_state == null) {
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
_seatFlightLog(
|
||||
'buffer request no state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=${normalizedRequest.queueTag} '
|
||||
'pending=${_pendingRequests.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_seatFlightLog(
|
||||
'enqueue via attached state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=${normalizedRequest.queueTag}',
|
||||
);
|
||||
_state!._enqueue(normalizedRequest);
|
||||
}
|
||||
|
||||
@ -91,20 +76,8 @@ class RoomGiftSeatFlightController {
|
||||
_trimPendingRequestsForTag(queueTag, maxTrackedRequests);
|
||||
_trimPendingRequestsOverflow();
|
||||
_pendingRequests.add(normalizedRequest);
|
||||
_seatFlightLog(
|
||||
'buffer limited request no state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=$queueTag '
|
||||
'pending=${_pendingRequests.length}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_seatFlightLog(
|
||||
'enqueue limited via attached state '
|
||||
'target=${normalizedRequest.targetUserId} '
|
||||
'queueTag=$queueTag '
|
||||
'max=$maxTrackedRequests',
|
||||
);
|
||||
_state!._enqueueLimited(
|
||||
normalizedRequest,
|
||||
maxTrackedRequests: maxTrackedRequests,
|
||||
@ -211,7 +184,6 @@ class RoomGiftSeatFlightController {
|
||||
|
||||
void _attach(_RoomGiftSeatFlightOverlayState state) {
|
||||
_state = state;
|
||||
_seatFlightLog('attach overlay pending=${_pendingRequests.length}');
|
||||
while (_pendingRequests.isNotEmpty) {
|
||||
state._enqueue(_pendingRequests.removeFirst());
|
||||
}
|
||||
@ -219,7 +191,6 @@ class RoomGiftSeatFlightController {
|
||||
|
||||
void _detach(_RoomGiftSeatFlightOverlayState state) {
|
||||
if (_state == state) {
|
||||
_seatFlightLog('detach overlay');
|
||||
_state = null;
|
||||
}
|
||||
}
|
||||
@ -291,13 +262,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_ensureCenterVisual(request);
|
||||
_trimQueuedRequestsOverflow();
|
||||
_queue.add(_QueuedRoomGiftSeatFlightRequest(request: request));
|
||||
_seatFlightLog(
|
||||
'state enqueue '
|
||||
'target=${request.targetUserId} '
|
||||
'queue=${_queue.length} '
|
||||
'queueTag=${request.queueTag} '
|
||||
'batchTag=${request.batchTag}',
|
||||
);
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
}
|
||||
@ -447,10 +411,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
_queue.isEmpty ||
|
||||
(expectedSessionToken != null &&
|
||||
expectedSessionToken != _sessionToken)) {
|
||||
_seatFlightLog(
|
||||
'skip start mounted=$mounted playing=$_isPlaying queue=${_queue.length} '
|
||||
'expected=$expectedSessionToken session=$_sessionToken',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -481,16 +441,9 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
_seatFlightLog(
|
||||
'start batch size=${queuedBatch.length} '
|
||||
'active=${activeFlights.length} '
|
||||
'unresolved=${unresolvedBatch.map((item) => item.request.targetUserId).join(",")} '
|
||||
'queueRemaining=${_queue.length}',
|
||||
final shouldRetryBatch = unresolvedBatch.any(
|
||||
(queuedRequest) => queuedRequest.retryCount < 6,
|
||||
);
|
||||
|
||||
final shouldRetryBatch =
|
||||
activeFlights.isEmpty &&
|
||||
unresolvedBatch.any((queuedRequest) => queuedRequest.retryCount < 6);
|
||||
if (shouldRetryBatch) {
|
||||
final retryBatch =
|
||||
queuedBatch
|
||||
@ -513,7 +466,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
}
|
||||
|
||||
if (activeFlights.isEmpty) {
|
||||
_seatFlightLog('drop batch no resolved targets');
|
||||
_syncIdleCenterVisual();
|
||||
_scheduleNextAnimation();
|
||||
return;
|
||||
@ -642,13 +594,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
final targetKey = widget.resolveTargetKey(targetUserId);
|
||||
final targetContext = targetKey?.currentContext;
|
||||
if (overlayContext == null || targetContext == null) {
|
||||
_seatFlightLog(
|
||||
'resolve target failed context '
|
||||
'target=$targetUserId '
|
||||
'hasOverlayContext=${overlayContext != null} '
|
||||
'hasTargetKey=${targetKey != null} '
|
||||
'hasTargetContext=${targetContext != null}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -658,12 +603,6 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
||||
targetBox is! RenderBox ||
|
||||
!overlayBox.hasSize ||
|
||||
!targetBox.hasSize) {
|
||||
_seatFlightLog(
|
||||
'resolve target failed render '
|
||||
'target=$targetUserId '
|
||||
'overlayBox=${overlayBox.runtimeType} '
|
||||
'targetBox=${targetBox.runtimeType}',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -18,14 +17,12 @@ class SCGiftMp4PlayRequest {
|
||||
required this.sourceType,
|
||||
required this.token,
|
||||
required this.muted,
|
||||
this.vapLayout,
|
||||
});
|
||||
|
||||
final String path;
|
||||
final SCGiftMp4SourceType sourceType;
|
||||
final int token;
|
||||
final bool muted;
|
||||
final Map<String, Object?>? vapLayout;
|
||||
}
|
||||
|
||||
class SCGiftMp4Controller extends ChangeNotifier {
|
||||
@ -53,13 +50,6 @@ class SCGiftMp4Controller extends ChangeNotifier {
|
||||
|
||||
SCGiftMp4PlayRequest? get request => _request;
|
||||
|
||||
static Future<void> preloadVapLayout({
|
||||
required String path,
|
||||
required SCGiftMp4SourceType sourceType,
|
||||
}) async {
|
||||
await _parseVapLayout(path: path, sourceType: sourceType);
|
||||
}
|
||||
|
||||
static void _installEventsHandler() {
|
||||
if (_eventsHandlerInstalled) {
|
||||
return;
|
||||
@ -104,10 +94,10 @@ class SCGiftMp4Controller extends ChangeNotifier {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<void> playAsset(String path) async =>
|
||||
Future<void> playAsset(String path) =>
|
||||
_play(path: path, sourceType: SCGiftMp4SourceType.asset);
|
||||
|
||||
Future<void> playFile(String path) async =>
|
||||
Future<void> playFile(String path) =>
|
||||
_play(path: path, sourceType: SCGiftMp4SourceType.file);
|
||||
|
||||
Future<void> _play({
|
||||
@ -120,10 +110,6 @@ class SCGiftMp4Controller extends ChangeNotifier {
|
||||
);
|
||||
return;
|
||||
}
|
||||
final layout = await _parseVapLayout(path: path, sourceType: sourceType);
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
_unregisterActiveView();
|
||||
_activeViewId = null;
|
||||
_request = SCGiftMp4PlayRequest(
|
||||
@ -131,7 +117,6 @@ class SCGiftMp4Controller extends ChangeNotifier {
|
||||
sourceType: sourceType,
|
||||
token: ++_nextToken,
|
||||
muted: _muted,
|
||||
vapLayout: layout,
|
||||
);
|
||||
debugPrint(
|
||||
'[GiftFx][mp4] request token=$_nextToken source=${sourceType.name} path=$path',
|
||||
@ -281,21 +266,16 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
final creationParams = <String, Object>{
|
||||
'path': request.path,
|
||||
'sourceType': request.sourceType.name,
|
||||
'token': request.token,
|
||||
'muted': request.muted,
|
||||
'renderMode': 'auto',
|
||||
};
|
||||
final vapLayout = request.vapLayout;
|
||||
if (vapLayout != null) {
|
||||
creationParams['vapLayout'] = vapLayout;
|
||||
}
|
||||
return AndroidView(
|
||||
key: ValueKey('gift-mp4-${request.token}-${request.path}'),
|
||||
viewType: 'gift_mp4_video_view',
|
||||
creationParams: creationParams,
|
||||
creationParams: <String, Object>{
|
||||
'path': request.path,
|
||||
'sourceType': request.sourceType.name,
|
||||
'token': request.token,
|
||||
'muted': request.muted,
|
||||
'renderMode': 'auto',
|
||||
},
|
||||
creationParamsCodec: const StandardMessageCodec(),
|
||||
onPlatformViewCreated:
|
||||
(viewId) =>
|
||||
@ -303,20 +283,15 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
|
||||
);
|
||||
}
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
|
||||
final creationParams = <String, Object>{
|
||||
'path': request.path,
|
||||
'sourceType': request.sourceType.name,
|
||||
'token': request.token,
|
||||
'muted': request.muted,
|
||||
};
|
||||
final vapLayout = request.vapLayout;
|
||||
if (vapLayout != null) {
|
||||
creationParams['vapLayout'] = vapLayout;
|
||||
}
|
||||
return UiKitView(
|
||||
key: ValueKey('gift-mp4-${request.token}-${request.path}'),
|
||||
viewType: 'gift_mp4_video_view',
|
||||
creationParams: creationParams,
|
||||
creationParams: <String, Object>{
|
||||
'path': request.path,
|
||||
'sourceType': request.sourceType.name,
|
||||
'token': request.token,
|
||||
'muted': request.muted,
|
||||
},
|
||||
creationParamsCodec: const StandardMessageCodec(),
|
||||
onPlatformViewCreated:
|
||||
(viewId) =>
|
||||
@ -338,223 +313,6 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
|
||||
}
|
||||
}
|
||||
|
||||
const int _vapLayoutScanBytes = 512 * 1024;
|
||||
final Map<String, Map<String, Object?>?> _vapLayoutCache =
|
||||
<String, Map<String, Object?>?>{};
|
||||
final Map<String, Future<Map<String, Object?>?>> _vapLayoutTasks =
|
||||
<String, Future<Map<String, Object?>?>>{};
|
||||
|
||||
Future<Map<String, Object?>?> _parseVapLayout({
|
||||
required String path,
|
||||
required SCGiftMp4SourceType sourceType,
|
||||
}) async {
|
||||
final cacheKey = '${sourceType.name}|$path';
|
||||
if (_vapLayoutCache.containsKey(cacheKey)) {
|
||||
return _vapLayoutCache[cacheKey];
|
||||
}
|
||||
final loadingTask = _vapLayoutTasks[cacheKey];
|
||||
if (loadingTask != null) {
|
||||
return loadingTask;
|
||||
}
|
||||
final future = _parseVapLayoutUncached(path: path, sourceType: sourceType);
|
||||
_vapLayoutTasks[cacheKey] = future;
|
||||
try {
|
||||
final layout = await future;
|
||||
_vapLayoutCache[cacheKey] = layout;
|
||||
return layout;
|
||||
} finally {
|
||||
_vapLayoutTasks.remove(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, Object?>?> _parseVapLayoutUncached({
|
||||
required String path,
|
||||
required SCGiftMp4SourceType sourceType,
|
||||
}) async {
|
||||
try {
|
||||
final bytes =
|
||||
sourceType == SCGiftMp4SourceType.asset
|
||||
? (await rootBundle.load(path)).buffer.asUint8List()
|
||||
: await _readFilePrefixBytes(path.removePrefix('file://'));
|
||||
final jsonText = _extractVapcJson(bytes);
|
||||
if (jsonText == null) {
|
||||
return null;
|
||||
}
|
||||
final root = jsonDecode(jsonText);
|
||||
if (root is! Map) {
|
||||
return null;
|
||||
}
|
||||
final info = root['info'];
|
||||
if (info is! Map) {
|
||||
return null;
|
||||
}
|
||||
final videoWidth =
|
||||
_readPositiveInt(info['videoW']) ??
|
||||
_readPositiveInt(info['video_width']);
|
||||
final videoHeight =
|
||||
_readPositiveInt(info['videoH']) ??
|
||||
_readPositiveInt(info['video_height']);
|
||||
if (videoWidth == null || videoHeight == null) {
|
||||
return null;
|
||||
}
|
||||
final color = _readFrame(info['rgbFrame']) ?? _readFrame(info['rgb_frame']);
|
||||
final alpha = _readFrame(info['aFrame']) ?? _readFrame(info['a_frame']);
|
||||
if (color == null || alpha == null) {
|
||||
return null;
|
||||
}
|
||||
final contentWidth = _readPositiveInt(info['w']) ?? videoWidth;
|
||||
final contentHeight = _readPositiveInt(info['h']) ?? videoHeight;
|
||||
final layout = <String, Object?>{
|
||||
'colorRect': _normalizeFrame(color, videoWidth, videoHeight),
|
||||
'alphaRect': _normalizeFrame(alpha, videoWidth, videoHeight),
|
||||
'hasAlphaMask': true,
|
||||
'contentAspectRatio': contentWidth / contentHeight,
|
||||
};
|
||||
debugPrint('[GiftFx][mp4] parsed vapLayout path=$path layout=$layout');
|
||||
return layout;
|
||||
} catch (error) {
|
||||
debugPrint('[GiftFx][mp4] parse vapLayout failed path=$path error=$error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> _readFilePrefixBytes(String path) async {
|
||||
final file = File(path);
|
||||
final length = await file.length();
|
||||
final readLength =
|
||||
length < _vapLayoutScanBytes ? length.toInt() : _vapLayoutScanBytes;
|
||||
if (readLength <= 0) {
|
||||
return Uint8List(0);
|
||||
}
|
||||
final randomAccessFile = await file.open();
|
||||
try {
|
||||
return await randomAccessFile.read(readLength);
|
||||
} finally {
|
||||
await randomAccessFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
String? _extractVapcJson(Uint8List bytes) {
|
||||
const marker = [0x76, 0x61, 0x70, 0x63]; // vapc
|
||||
final markerIndex = _indexOfBytes(bytes, marker, 0);
|
||||
if (markerIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
final searchEnd =
|
||||
(markerIndex + marker.length + _vapLayoutScanBytes)
|
||||
.clamp(0, bytes.length)
|
||||
.toInt();
|
||||
for (var index = markerIndex + marker.length; index < searchEnd; index++) {
|
||||
if (bytes[index] != 0x7b) {
|
||||
continue;
|
||||
}
|
||||
final end = _findJsonObjectEnd(bytes, index, searchEnd);
|
||||
if (end > index) {
|
||||
return utf8.decode(bytes.sublist(index, end + 1));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int _indexOfBytes(Uint8List bytes, List<int> pattern, int start) {
|
||||
if (pattern.isEmpty || bytes.length < pattern.length) {
|
||||
return -1;
|
||||
}
|
||||
for (var index = start; index <= bytes.length - pattern.length; index++) {
|
||||
var matches = true;
|
||||
for (var patternIndex = 0; patternIndex < pattern.length; patternIndex++) {
|
||||
if (bytes[index + patternIndex] != pattern[patternIndex]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int _findJsonObjectEnd(Uint8List bytes, int start, int maxEnd) {
|
||||
var depth = 0;
|
||||
var inString = false;
|
||||
var escaping = false;
|
||||
for (var index = start; index < maxEnd; index++) {
|
||||
final byte = bytes[index];
|
||||
if (inString) {
|
||||
if (escaping) {
|
||||
escaping = false;
|
||||
} else if (byte == 0x5c) {
|
||||
escaping = true;
|
||||
} else if (byte == 0x22) {
|
||||
inString = false;
|
||||
}
|
||||
} else if (byte == 0x22) {
|
||||
inString = true;
|
||||
} else if (byte == 0x7b) {
|
||||
depth += 1;
|
||||
} else if (byte == 0x7d) {
|
||||
depth -= 1;
|
||||
if (depth == 0) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int? _readPositiveInt(Object? value) {
|
||||
final resolved =
|
||||
value is num
|
||||
? value.toInt()
|
||||
: value is String
|
||||
? int.tryParse(value.trim())
|
||||
: null;
|
||||
return resolved != null && resolved > 0 ? resolved : null;
|
||||
}
|
||||
|
||||
List<int>? _readFrame(Object? value) {
|
||||
final values =
|
||||
value is List
|
||||
? value.map(_readPositiveInt).toList()
|
||||
: value is String
|
||||
? value.split(',').map((item) => int.tryParse(item.trim())).toList()
|
||||
: null;
|
||||
if (values == null ||
|
||||
values.length < 4 ||
|
||||
values[0] == null ||
|
||||
values[1] == null ||
|
||||
values[2] == null ||
|
||||
values[3] == null ||
|
||||
values[2]! <= 0 ||
|
||||
values[3]! <= 0) {
|
||||
return null;
|
||||
}
|
||||
return [values[0]!, values[1]!, values[2]!, values[3]!];
|
||||
}
|
||||
|
||||
List<double> _normalizeFrame(
|
||||
List<int> values,
|
||||
int videoWidth,
|
||||
int videoHeight,
|
||||
) {
|
||||
return <double>[
|
||||
values[0] / videoWidth,
|
||||
values[1] / videoHeight,
|
||||
values[2] / videoWidth,
|
||||
values[3] / videoHeight,
|
||||
];
|
||||
}
|
||||
|
||||
extension on String {
|
||||
String removePrefix(String prefix) {
|
||||
if (startsWith(prefix)) {
|
||||
return substring(prefix.length);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class _FallbackMp4EffectVideo extends StatefulWidget {
|
||||
const _FallbackMp4EffectVideo({
|
||||
required this.request,
|
||||
|
||||
@ -14,7 +14,6 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onAnimationCompleted,
|
||||
this.debugDisableAutoSlide = false,
|
||||
});
|
||||
|
||||
static const int floatingType = 7;
|
||||
@ -25,7 +24,6 @@ class FloatingLuckyGiftGlobalScreenWidget extends StatefulWidget {
|
||||
|
||||
final SCFloatingMessage message;
|
||||
final VoidCallback onAnimationCompleted;
|
||||
final bool debugDisableAutoSlide;
|
||||
|
||||
@override
|
||||
State<FloatingLuckyGiftGlobalScreenWidget> createState() =>
|
||||
@ -75,7 +73,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && !widget.debugDisableAutoSlide) {
|
||||
if (mounted) {
|
||||
_controller.forward();
|
||||
}
|
||||
});
|
||||
@ -96,19 +94,29 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
onTap: _handleTap,
|
||||
onHorizontalDragEnd: _handleHorizontalDragEnd,
|
||||
child: SlideTransition(
|
||||
position:
|
||||
widget.debugDisableAutoSlide
|
||||
? const AlwaysStoppedAnimation(Offset.zero)
|
||||
: (_isSwipeAnimating ? _swipeAnimation : _offsetAnimation),
|
||||
position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation,
|
||||
child: SizedBox(
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(left: 70.w, top: 34.w, child: _buildAvatar()),
|
||||
Positioned.fill(child: _buildBackground()),
|
||||
Positioned.fill(child: _buildContentOverlay()),
|
||||
Positioned(left: 30.w, top: 22.w, child: _buildAvatar()),
|
||||
Positioned(
|
||||
left: 88.w,
|
||||
top: 26.w,
|
||||
width: 160.w,
|
||||
height: 45.w,
|
||||
child: _buildTextBlock(),
|
||||
),
|
||||
Positioned(
|
||||
right: 23.w,
|
||||
top: 24.w,
|
||||
width: 61.w,
|
||||
height: 45.w,
|
||||
child: _buildMultiple(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -122,7 +130,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
resource: FloatingLuckyGiftGlobalScreenWidget.backgroundPagAssetPath,
|
||||
width: FloatingLuckyGiftGlobalScreenWidget.bannerWidth.w,
|
||||
height: FloatingLuckyGiftGlobalScreenWidget.bannerHeight.w,
|
||||
fit: BoxFit.contain,
|
||||
fit: BoxFit.fill,
|
||||
clipBehavior: Clip.none,
|
||||
defaultBuilder: (_) => _buildFallbackBackground(),
|
||||
);
|
||||
@ -142,28 +150,6 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentOverlay() {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned(
|
||||
left: 110.w,
|
||||
top: 24.w,
|
||||
width: 120.w,
|
||||
height: 46.w,
|
||||
child: _buildTextBlock(),
|
||||
),
|
||||
Positioned(
|
||||
left: 220.w,
|
||||
top: 24.w,
|
||||
width: 70.w,
|
||||
height: 46.w,
|
||||
child: _buildMultiple(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatar() {
|
||||
const fallbackAsset = "sc_images/general/sc_icon_avar_defalt.png";
|
||||
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
|
||||
@ -171,28 +157,37 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
avatarUrl.isEmpty
|
||||
? Image.asset(
|
||||
fallbackAsset,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Image(
|
||||
image: buildCachedImageProvider(
|
||||
avatarUrl,
|
||||
logicalWidth: 38.w,
|
||||
logicalHeight: 38.w,
|
||||
logicalWidth: 48.w,
|
||||
logicalHeight: 48.w,
|
||||
),
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => Image.asset(
|
||||
fallbackAsset,
|
||||
width: 38.w,
|
||||
height: 38.w,
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
return ClipOval(child: avatar);
|
||||
return Container(
|
||||
width: 48.w,
|
||||
height: 48.w,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: const Color(0xFFFFF4C2), width: 1.5.w),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: avatar,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextBlock() {
|
||||
@ -206,20 +201,20 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
title,
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 19.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: _textShadows(),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.w),
|
||||
SizedBox(height: 7.w),
|
||||
_singleLineText(
|
||||
subtitle,
|
||||
TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10.5.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
shadows: _textShadows(),
|
||||
@ -239,7 +234,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
textScaler: TextScaler.noScaling,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 23.sp,
|
||||
fontSize: 32.sp,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
|
||||
@ -1345,7 +1345,6 @@ class _RocketImage extends StatelessWidget {
|
||||
? CustomCachedImage(
|
||||
imageUrl: source,
|
||||
fit: fit,
|
||||
debugName: debugName,
|
||||
errorWidget:
|
||||
allowStaticFallback ? null : const SizedBox.shrink(),
|
||||
)
|
||||
@ -1356,7 +1355,6 @@ class _RocketImage extends StatelessWidget {
|
||||
return CustomCachedImage(
|
||||
imageUrl: source,
|
||||
fit: fit,
|
||||
debugName: debugName,
|
||||
errorWidget: allowStaticFallback ? null : const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -377,42 +377,41 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
}
|
||||
final imageReplacementsByName =
|
||||
_imageReplacementsByName ?? const <String, Uint8List>{};
|
||||
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: assetName != null,
|
||||
hasBytes: bytes != null,
|
||||
);
|
||||
final useNativeAssetView =
|
||||
assetName != null && Platform.isIOS && widget.loop;
|
||||
final pagView =
|
||||
assetName != null
|
||||
? PAGView.asset(
|
||||
assetName,
|
||||
key: _pagKey,
|
||||
width: useNativeView ? widget.width : null,
|
||||
height: useNativeView ? widget.height : null,
|
||||
autoPlay: useNativeView,
|
||||
usePlatformView: useNativeView,
|
||||
width: useNativeAssetView ? widget.width : null,
|
||||
height: useNativeAssetView ? widget.height : null,
|
||||
autoPlay: useNativeAssetView,
|
||||
usePlatformView: useNativeAssetView,
|
||||
repeatCount: repeatCount,
|
||||
onInit: useNativeView ? null : _handleInit,
|
||||
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||
onInit: useNativeAssetView ? null : _handleInit,
|
||||
onAnimationStart:
|
||||
useNativeAssetView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeAssetView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel:
|
||||
useNativeAssetView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat:
|
||||
useNativeAssetView ? null : _handleAnimationRepeat,
|
||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||
imageReplacementsByName: imageReplacementsByName,
|
||||
)
|
||||
: PAGView.bytes(
|
||||
bytes,
|
||||
key: _pagKey,
|
||||
width: useNativeView ? widget.width : null,
|
||||
height: useNativeView ? widget.height : null,
|
||||
autoPlay: useNativeView,
|
||||
usePlatformView: useNativeView,
|
||||
width: null,
|
||||
height: null,
|
||||
autoPlay: false,
|
||||
repeatCount: repeatCount,
|
||||
onInit: useNativeView ? null : _handleInit,
|
||||
onAnimationStart: useNativeView ? null : _handleAnimationStart,
|
||||
onAnimationEnd: useNativeView ? null : _handleAnimationEnd,
|
||||
onAnimationCancel: useNativeView ? null : _handleAnimationCancel,
|
||||
onAnimationRepeat: useNativeView ? null : _handleAnimationRepeat,
|
||||
onInit: _handleInit,
|
||||
onAnimationStart: _handleAnimationStart,
|
||||
onAnimationEnd: _handleAnimationEnd,
|
||||
onAnimationCancel: _handleAnimationCancel,
|
||||
onAnimationRepeat: _handleAnimationRepeat,
|
||||
defaultBuilder: (_) => const SizedBox(width: 1, height: 1),
|
||||
imageReplacementsByName: imageReplacementsByName,
|
||||
);
|
||||
@ -423,7 +422,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
widget.clipBehavior == Clip.none ? Clip.none : Clip.hardEdge,
|
||||
children: [
|
||||
if (visibleUnderlay != null) visibleUnderlay,
|
||||
if (useNativeView)
|
||||
if (useNativeAssetView)
|
||||
Positioned.fill(child: pagView)
|
||||
else
|
||||
_scaledPagView(pagView),
|
||||
@ -476,11 +475,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
urlsByName: imageReplacementUrls,
|
||||
),
|
||||
);
|
||||
if (_shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: true,
|
||||
hasBytes: false,
|
||||
)) {
|
||||
if (widget.loop) {
|
||||
_startWatchdog?.cancel();
|
||||
_initialized = true;
|
||||
}
|
||||
@ -535,25 +530,13 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
'head=${_pagByteHead(bytes)} '
|
||||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||||
);
|
||||
final useNativeView = _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: Platform.isIOS,
|
||||
hasAsset: false,
|
||||
hasBytes: true,
|
||||
);
|
||||
setState(() {
|
||||
_pagBytes = bytes;
|
||||
_imageReplacementsByName = imageReplacements;
|
||||
_releasedFromMemory = false;
|
||||
if (useNativeView) {
|
||||
_initialized = true;
|
||||
}
|
||||
});
|
||||
_scheduleMemoryRelease(path);
|
||||
if (useNativeView) {
|
||||
_startWatchdog?.cancel();
|
||||
} else {
|
||||
_restartWatchdog();
|
||||
}
|
||||
_restartWatchdog();
|
||||
} catch (error) {
|
||||
if (!_isCurrentResource(path, signature)) {
|
||||
return;
|
||||
@ -924,27 +907,6 @@ String debugRoomRocketPagSourceKind(String resource) {
|
||||
return _pagSourceKind(resource);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
bool debugShouldUseNativeRoomRocketPagView({
|
||||
required bool isIOS,
|
||||
required bool hasAsset,
|
||||
required bool hasBytes,
|
||||
}) {
|
||||
return _shouldUseNativeRoomRocketPagView(
|
||||
isIOS: isIOS,
|
||||
hasAsset: hasAsset,
|
||||
hasBytes: hasBytes,
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldUseNativeRoomRocketPagView({
|
||||
required bool isIOS,
|
||||
required bool hasAsset,
|
||||
required bool hasBytes,
|
||||
}) {
|
||||
return isIOS && (hasAsset || hasBytes);
|
||||
}
|
||||
|
||||
Future<Uint8List> _readPagBytes(String value) async {
|
||||
final normalized = _normalizePagResource(value);
|
||||
final uri = _pagNetworkUri(normalized);
|
||||
|
||||
@ -38,11 +38,9 @@ class RoomGiftComboFloatingLayer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.read<RtcProvider>();
|
||||
return AnimatedBuilder(
|
||||
animation: provider,
|
||||
builder: (context, child) {
|
||||
final showMic = _shouldShowRoomBottomMic(provider);
|
||||
return Selector<RtcProvider, bool>(
|
||||
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
|
||||
builder: (context, showMic, child) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = _resolveRoomBottomInputWidth(
|
||||
@ -83,16 +81,15 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.read<RtcProvider>();
|
||||
return SizedBox(
|
||||
height: _floatingButtonHostHeight.w,
|
||||
child: AnimatedBuilder(
|
||||
animation: provider,
|
||||
builder: (context, child) {
|
||||
final bottomSnapshot = _RoomBottomSnapshot(
|
||||
showMic: _shouldShowRoomBottomMic(provider),
|
||||
isMic: provider.isMic,
|
||||
);
|
||||
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomBottomSnapshot(
|
||||
showMic: _shouldShowRoomBottomMic(provider),
|
||||
isMic: provider.isMic,
|
||||
),
|
||||
builder: (context, bottomSnapshot, child) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = _resolveRoomBottomInputWidth(
|
||||
@ -326,7 +323,11 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
unawaited(context.read<RtcProvider>().toggleSelfMicMutedFromBottom());
|
||||
final provider = context.read<RtcProvider>();
|
||||
setState(() {
|
||||
provider.isMic = !provider.isMic;
|
||||
});
|
||||
unawaited(provider.setSelfMicMutedFromBottom(provider.isMic));
|
||||
},
|
||||
child: RoomBottomCircleAction(
|
||||
child: Image.asset(
|
||||
|
||||
@ -1,31 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||
|
||||
const Duration _roomEmojiGifRestartInterval = Duration(milliseconds: 3500);
|
||||
|
||||
String _roomEmojiCacheKey(String resource, Object token) {
|
||||
return 'room_emoji_${base64UrlEncode(utf8.encode('$resource|$token'))}';
|
||||
}
|
||||
|
||||
int? _resolveRoomEmojiImageCacheDimension(double? logicalSize) {
|
||||
if (logicalSize == null || !logicalSize.isFinite || logicalSize <= 0) {
|
||||
return null;
|
||||
}
|
||||
final devicePixelRatio =
|
||||
PlatformDispatcher.instance.implicitView?.devicePixelRatio ?? 2.0;
|
||||
final maxDimension = SCGlobalConfig.isLowPerformanceDevice ? 1280 : 1920;
|
||||
final pixelSize = (logicalSize * devicePixelRatio).round();
|
||||
return pixelSize.clamp(1, maxDimension).toInt();
|
||||
}
|
||||
|
||||
class RoomEmojiAssetImage extends StatefulWidget {
|
||||
const RoomEmojiAssetImage({
|
||||
@ -47,16 +20,12 @@ class RoomEmojiAssetImage extends StatefulWidget {
|
||||
|
||||
class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
static final Set<String> _loggedNetworkLoading = <String>{};
|
||||
static final Set<String> _loggedNetworkReady = <String>{};
|
||||
static final Set<String> _loggedNetworkError = <String>{};
|
||||
|
||||
AssetImage? _imageProvider;
|
||||
late AssetImage _imageProvider;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_syncImageProvider();
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -69,7 +38,7 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
void didUpdateWidget(covariant RoomEmojiAssetImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.asset != widget.asset) {
|
||||
_syncImageProvider();
|
||||
_imageProvider = AssetImage(widget.asset);
|
||||
_precache();
|
||||
}
|
||||
}
|
||||
@ -77,44 +46,9 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final resource = widget.asset.trim();
|
||||
if (resource.isEmpty) {
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
if (SCNetworkSvgaWidget.isSvga(resource)) {
|
||||
return RepaintBoundary(
|
||||
child: SCNetworkSvgaWidget(
|
||||
resource: resource,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
loop: true,
|
||||
fit: widget.fit,
|
||||
fallback: _buildNetworkFailure(resource, 'svga failed'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_isGif(resource)) {
|
||||
return RepaintBoundary(
|
||||
child: _RestartingGifImage(
|
||||
resource: resource,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
isLocalAsset: _isLocalAsset(resource),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!_isLocalAsset(resource)) {
|
||||
return RepaintBoundary(child: _buildNetworkImage(resource));
|
||||
}
|
||||
|
||||
final imageProvider = _imageProvider;
|
||||
if (imageProvider == null) {
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
return RepaintBoundary(
|
||||
child: Image(
|
||||
image: imageProvider,
|
||||
image: _imageProvider,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
@ -124,299 +58,10 @@ class _RoomEmojiAssetImageState extends State<RoomEmojiAssetImage>
|
||||
);
|
||||
}
|
||||
|
||||
void _syncImageProvider() {
|
||||
final resource = widget.asset.trim();
|
||||
_imageProvider = _isLocalAsset(resource) ? AssetImage(resource) : null;
|
||||
}
|
||||
|
||||
void _precache() {
|
||||
final imageProvider = _imageProvider;
|
||||
if (imageProvider == null) {
|
||||
return;
|
||||
}
|
||||
precacheImage(imageProvider, context);
|
||||
}
|
||||
|
||||
bool _isLocalAsset(String resource) {
|
||||
return resource.startsWith("assets/") || resource.startsWith("sc_images/");
|
||||
}
|
||||
|
||||
bool _isGif(String resource) {
|
||||
final lower = resource.trim().toLowerCase();
|
||||
return lower.endsWith('.gif') || lower.contains('.gif?');
|
||||
}
|
||||
|
||||
Widget _buildNetworkImage(String resource) {
|
||||
final normalizedUrl = normalizeImageResourceUrl(resource);
|
||||
return ExtendedImage.network(
|
||||
normalizedUrl,
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
clearMemoryCacheWhenDispose: false,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
gaplessPlayback: true,
|
||||
loadStateChanged: (state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
_logNetworkOnce(_loggedNetworkLoading, 'loading', normalizedUrl);
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: const Center(child: SCRotatingDotsLoading()),
|
||||
);
|
||||
case LoadState.completed:
|
||||
final imageInfo = state.extendedImageInfo;
|
||||
final image = imageInfo?.image;
|
||||
final size =
|
||||
image == null ? 'unknown' : '${image.width}x${image.height}';
|
||||
_logNetworkOnce(
|
||||
_loggedNetworkReady,
|
||||
'ready size=$size',
|
||||
normalizedUrl,
|
||||
);
|
||||
return null;
|
||||
case LoadState.failed:
|
||||
final error = state.lastException ?? 'unknown';
|
||||
_logNetworkOnce(
|
||||
_loggedNetworkError,
|
||||
'failed error=$error',
|
||||
normalizedUrl,
|
||||
);
|
||||
return _buildNetworkFailure(normalizedUrl, 'image failed');
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNetworkFailure(String resource, String reason) {
|
||||
_logNetworkOnce(
|
||||
_loggedNetworkError,
|
||||
reason,
|
||||
normalizeImageResourceUrl(resource),
|
||||
);
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
|
||||
void _logNetworkOnce(Set<String> bucket, String message, String url) {
|
||||
if (!kDebugMode || !bucket.add('$message|$url')) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomEmoji][image] $message url=$url');
|
||||
precacheImage(_imageProvider, context);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class _RestartingGifImage extends StatefulWidget {
|
||||
const _RestartingGifImage({
|
||||
required this.resource,
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.fit,
|
||||
required this.isLocalAsset,
|
||||
});
|
||||
|
||||
final String resource;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
final bool isLocalAsset;
|
||||
|
||||
@override
|
||||
State<_RestartingGifImage> createState() => _RestartingGifImageState();
|
||||
}
|
||||
|
||||
class _RestartingGifImageState extends State<_RestartingGifImage> {
|
||||
static final Set<String> _loggedLoading = <String>{};
|
||||
static final Set<String> _loggedReady = <String>{};
|
||||
static final Set<String> _loggedError = <String>{};
|
||||
|
||||
Timer? _restartTimer;
|
||||
int _restartToken = 0;
|
||||
int _visibleToken = 0;
|
||||
bool _hasCompletedOnce = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.isLocalAsset) {
|
||||
_startRestartTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _RestartingGifImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.resource != widget.resource ||
|
||||
oldWidget.isLocalAsset != widget.isLocalAsset) {
|
||||
_restartToken = 0;
|
||||
_visibleToken = 0;
|
||||
_hasCompletedOnce = false;
|
||||
_restartTimer?.cancel();
|
||||
if (widget.isLocalAsset) {
|
||||
_startRestartTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_restartTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resource = widget.resource.trim();
|
||||
if (resource.isEmpty) {
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
if (widget.isLocalAsset) {
|
||||
return Image.asset(
|
||||
resource,
|
||||
key: ValueKey('room_emoji_gif_asset_${resource}_$_restartToken'),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
gaplessPlayback: true,
|
||||
filterQuality: FilterQuality.low,
|
||||
);
|
||||
}
|
||||
|
||||
final normalizedUrl = normalizeImageResourceUrl(resource);
|
||||
final foreground = _buildNetworkGifForeground(normalizedUrl);
|
||||
if (!_hasCompletedOnce) {
|
||||
return foreground;
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
_buildNetworkGifPlaceholder(normalizedUrl, _visibleToken),
|
||||
foreground,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNetworkGifPlaceholder(String normalizedUrl, int token) {
|
||||
return ExtendedImage.network(
|
||||
normalizedUrl,
|
||||
key: ValueKey('room_emoji_gif_placeholder_${normalizedUrl}_$token'),
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
cacheKey: _roomEmojiCacheKey(normalizedUrl, token % 2),
|
||||
clearMemoryCacheWhenDispose: false,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
gaplessPlayback: true,
|
||||
loadStateChanged: (state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.completed:
|
||||
return null;
|
||||
case LoadState.loading:
|
||||
case LoadState.failed:
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNetworkGifForeground(String normalizedUrl) {
|
||||
return ExtendedImage.network(
|
||||
normalizedUrl,
|
||||
key: ValueKey(
|
||||
'room_emoji_gif_foreground_${normalizedUrl}_$_restartToken',
|
||||
),
|
||||
headers: buildNetworkImageHeaders(normalizedUrl),
|
||||
cache: true,
|
||||
cacheKey: _roomEmojiCacheKey(normalizedUrl, _restartToken % 2),
|
||||
clearMemoryCacheWhenDispose: true,
|
||||
clearMemoryCacheIfFailed: true,
|
||||
cacheWidth: _resolveRoomEmojiImageCacheDimension(widget.width),
|
||||
cacheHeight: _resolveRoomEmojiImageCacheDimension(widget.height),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
gaplessPlayback: true,
|
||||
loadStateChanged: (state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
_debugGifOnce(_loggedLoading, 'loading', normalizedUrl);
|
||||
if (_hasCompletedOnce) {
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: const Center(child: SCRotatingDotsLoading()),
|
||||
);
|
||||
case LoadState.completed:
|
||||
final image = state.extendedImageInfo?.image;
|
||||
final size =
|
||||
image == null ? 'unknown' : '${image.width}x${image.height}';
|
||||
_debugGifOnce(_loggedReady, 'ready size=$size', normalizedUrl);
|
||||
if (!_hasCompletedOnce) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_hasCompletedOnce = true;
|
||||
_visibleToken = _restartToken;
|
||||
});
|
||||
});
|
||||
} else if (_visibleToken != _restartToken) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_visibleToken = _restartToken;
|
||||
});
|
||||
});
|
||||
}
|
||||
_startRestartTimer();
|
||||
return null;
|
||||
case LoadState.failed:
|
||||
final error = state.lastException ?? 'unknown';
|
||||
_debugGifOnce(_loggedError, 'failed error=$error', normalizedUrl);
|
||||
return SizedBox(width: widget.width, height: widget.height);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startRestartTimer() {
|
||||
if (_restartTimer?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
_restartTimer?.cancel();
|
||||
_restartTimer = Timer.periodic(_roomEmojiGifRestartInterval, (_) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_restartToken += 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void _debugGifOnce(
|
||||
Set<String> bucket,
|
||||
String message,
|
||||
String resource,
|
||||
) {
|
||||
if (!kDebugMode || !bucket.add('$message|$resource')) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomEmoji][gif] $message url=$resource');
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,8 +9,6 @@ import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
import 'package:yumi/shared/tools/sc_keybord_util.dart';
|
||||
@ -33,10 +31,45 @@ class RoomMsgInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
|
||||
_RoomEmojiCategory(
|
||||
id: "fluent",
|
||||
iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
assets: [
|
||||
"sc_images/room/emoji/fluent_emoji_01.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_02.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_03.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_04.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_05.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_06.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_07.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_08.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_09.webp",
|
||||
"sc_images/room/emoji/fluent_emoji_10.webp",
|
||||
],
|
||||
),
|
||||
_RoomEmojiCategory(
|
||||
id: "monkey",
|
||||
iconAsset: "sc_images/room/emoji/monkey_1.gif",
|
||||
assets: [
|
||||
"sc_images/room/emoji/monkey_1.gif",
|
||||
"sc_images/room/emoji/monkey_2.gif",
|
||||
"sc_images/room/emoji/monkey_3.gif",
|
||||
"sc_images/room/emoji/monkey_4.gif",
|
||||
"sc_images/room/emoji/monkey_5.gif",
|
||||
"sc_images/room/emoji/monkey_6.gif",
|
||||
"sc_images/room/emoji/monkey_7.gif",
|
||||
"sc_images/room/emoji/monkey_8.gif",
|
||||
"sc_images/room/emoji/monkey_9.gif",
|
||||
"sc_images/room/emoji/monkey_10.gif",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
bool showSend = false;
|
||||
bool showEmoji = false;
|
||||
bool _emojiAssetsPrecached = false;
|
||||
int _selectedEmojiCategoryIndex = 0;
|
||||
String? _lastEmojiPanelLogSignature;
|
||||
|
||||
final FocusNode msgNode = FocusNode();
|
||||
final TextEditingController controller = TextEditingController();
|
||||
@ -65,7 +98,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (showEmoji) {
|
||||
_loadRoomEmojisIfNeeded();
|
||||
_precacheRoomEmojis();
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,19 +247,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
}
|
||||
|
||||
Widget _buildEmojiPanel() {
|
||||
final categories = _resolveRoomEmojiCategories(context);
|
||||
_logEmojiPanelState(categories);
|
||||
if (categories.isEmpty) {
|
||||
return Container(
|
||||
height: 220.w,
|
||||
decoration: BoxDecoration(
|
||||
color: _panelBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(height(5)),
|
||||
),
|
||||
);
|
||||
}
|
||||
final selectedIndex = _clampedEmojiCategoryIndex(categories);
|
||||
final selectedCategory = categories[selectedIndex];
|
||||
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
|
||||
return Container(
|
||||
height: 220.w,
|
||||
decoration: BoxDecoration(
|
||||
@ -244,18 +265,18 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
crossAxisSpacing: 18.w,
|
||||
mainAxisSpacing: 12.w,
|
||||
),
|
||||
itemCount: selectedCategory.items.length,
|
||||
itemCount: selectedCategory.assets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final emojiItem = selectedCategory.items[index];
|
||||
final emojiAsset = selectedCategory.assets[index];
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
_sendRoomEmoji(emojiItem.sendResource);
|
||||
_sendRoomEmoji(emojiAsset);
|
||||
},
|
||||
child: Center(
|
||||
child: RoomEmojiAssetImage(
|
||||
key: ValueKey(emojiItem.displayResource),
|
||||
asset: emojiItem.displayResource,
|
||||
key: ValueKey(emojiAsset),
|
||||
asset: emojiAsset,
|
||||
width: 42.w,
|
||||
height: 42.w,
|
||||
),
|
||||
@ -264,16 +285,13 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildEmojiCategoryBar(categories, selectedIndex),
|
||||
_buildEmojiCategoryBar(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmojiCategoryBar(
|
||||
List<_RoomEmojiCategory> categories,
|
||||
int selectedIndex,
|
||||
) {
|
||||
Widget _buildEmojiCategoryBar() {
|
||||
return Container(
|
||||
height: 50.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
|
||||
@ -285,11 +303,11 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
),
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: categories.length,
|
||||
itemCount: _roomEmojiCategories.length,
|
||||
separatorBuilder: (_, __) => SizedBox(width: 10.w),
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final selected = index == selectedIndex;
|
||||
final category = _roomEmojiCategories[index];
|
||||
final selected = index == _selectedEmojiCategoryIndex;
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
@ -322,7 +340,7 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
child: Center(
|
||||
child: RoomEmojiAssetImage(
|
||||
key: ValueKey("emoji_category_${category.id}"),
|
||||
asset: category.iconResource,
|
||||
asset: category.iconAsset,
|
||||
width: 26.w,
|
||||
height: 26.w,
|
||||
),
|
||||
@ -344,59 +362,23 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
}
|
||||
|
||||
SCKeybordUtil.hide(context);
|
||||
_roomEmojiInputDebugLog('toggle open: request remote emoji load');
|
||||
_loadRoomEmojisIfNeeded(forceWhenEmpty: true);
|
||||
_precacheRoomEmojis();
|
||||
setState(() {
|
||||
showEmoji = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _loadRoomEmojisIfNeeded({bool forceWhenEmpty = false}) {
|
||||
final manager = Provider.of<SCAppGeneralManager>(context, listen: false);
|
||||
_roomEmojiInputDebugLog(
|
||||
'load if needed loaded=${manager.hasRoomEmojisLoaded} '
|
||||
'loading=${manager.isFetchingRoomEmojis} '
|
||||
'groups=${manager.roomEmojiGroups.length} '
|
||||
'sourceAll=${manager.roomEmojisLoadedFromAll}',
|
||||
);
|
||||
final forceRefresh =
|
||||
forceWhenEmpty &&
|
||||
manager.hasRoomEmojisLoaded &&
|
||||
manager.roomEmojiGroups.isEmpty;
|
||||
manager.emojiAll(forceRefresh: forceRefresh);
|
||||
}
|
||||
|
||||
List<_RoomEmojiCategory> _resolveRoomEmojiCategories(BuildContext context) {
|
||||
final generalManager = Provider.of<SCAppGeneralManager>(context);
|
||||
return generalManager.roomEmojiGroups
|
||||
.map(_RoomEmojiCategory.fromRemote)
|
||||
.where((category) => category.items.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _logEmojiPanelState(List<_RoomEmojiCategory> categories) {
|
||||
final manager = Provider.of<SCAppGeneralManager>(context, listen: false);
|
||||
final signature =
|
||||
'show=$showEmoji loaded=${manager.hasRoomEmojisLoaded} '
|
||||
'loading=${manager.isFetchingRoomEmojis} '
|
||||
'sourceAll=${manager.roomEmojisLoadedFromAll} '
|
||||
'rawGroups=${manager.roomEmojiGroups.length} '
|
||||
'renderGroups=${categories.length} '
|
||||
'renderEmojiTotal=${_countRenderedEmojiItems(categories)}';
|
||||
if (_lastEmojiPanelLogSignature == signature) {
|
||||
void _precacheRoomEmojis() {
|
||||
if (_emojiAssetsPrecached) {
|
||||
return;
|
||||
}
|
||||
_lastEmojiPanelLogSignature = signature;
|
||||
_roomEmojiInputDebugLog(
|
||||
'panel state $signature sample=${_renderedEmojiCategorySample(categories)}',
|
||||
);
|
||||
}
|
||||
|
||||
int _clampedEmojiCategoryIndex(List<_RoomEmojiCategory> categories) {
|
||||
if (_selectedEmojiCategoryIndex < categories.length) {
|
||||
return _selectedEmojiCategoryIndex;
|
||||
_emojiAssetsPrecached = true;
|
||||
for (final category in _roomEmojiCategories) {
|
||||
precacheImage(AssetImage(category.iconAsset), context);
|
||||
for (final asset in category.assets) {
|
||||
precacheImage(AssetImage(asset), context);
|
||||
}
|
||||
}
|
||||
return categories.isEmpty ? 0 : categories.length - 1;
|
||||
}
|
||||
|
||||
void _sendRoomEmoji(String emojiAsset) {
|
||||
@ -453,76 +435,13 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||
class _RoomEmojiCategory {
|
||||
const _RoomEmojiCategory({
|
||||
required this.id,
|
||||
required this.iconResource,
|
||||
required this.items,
|
||||
required this.iconAsset,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
factory _RoomEmojiCategory.fromRemote(SCRoomEmojiRes group) {
|
||||
final items =
|
||||
(group.emojis ?? const <Emojis>[])
|
||||
.map(_RoomEmojiItem.fromRemote)
|
||||
.where((item) => item.sendResource.isNotEmpty)
|
||||
.toList();
|
||||
final firstPreview =
|
||||
items.isNotEmpty ? items.first.displayResource.trim() : "";
|
||||
final cover = group.cover?.trim() ?? "";
|
||||
final id =
|
||||
(group.id ?? group.groupCode ?? group.groupName ?? firstPreview).trim();
|
||||
return _RoomEmojiCategory(
|
||||
id: id.isEmpty ? firstPreview : id,
|
||||
iconResource: cover.isNotEmpty ? cover : firstPreview,
|
||||
items: items,
|
||||
);
|
||||
}
|
||||
|
||||
final String id;
|
||||
final String iconResource;
|
||||
final List<_RoomEmojiItem> items;
|
||||
}
|
||||
|
||||
class _RoomEmojiItem {
|
||||
const _RoomEmojiItem({
|
||||
required this.displayResource,
|
||||
required this.sendResource,
|
||||
});
|
||||
|
||||
factory _RoomEmojiItem.fromRemote(Emojis emoji) {
|
||||
final display = emoji.previewUrl.trim();
|
||||
final send = emoji.sendUrl.trim();
|
||||
return _RoomEmojiItem(
|
||||
displayResource: display.isNotEmpty ? display : send,
|
||||
sendResource: send.isNotEmpty ? send : display,
|
||||
);
|
||||
}
|
||||
|
||||
final String displayResource;
|
||||
final String sendResource;
|
||||
}
|
||||
|
||||
void _roomEmojiInputDebugLog(String message) {
|
||||
debugPrint('[RoomEmoji][input] $message');
|
||||
}
|
||||
|
||||
int _countRenderedEmojiItems(List<_RoomEmojiCategory> categories) {
|
||||
return categories.fold<int>(
|
||||
0,
|
||||
(total, category) => total + category.items.length,
|
||||
);
|
||||
}
|
||||
|
||||
String _renderedEmojiCategorySample(List<_RoomEmojiCategory> categories) {
|
||||
if (categories.isEmpty) {
|
||||
return '[]';
|
||||
}
|
||||
return categories
|
||||
.take(3)
|
||||
.map((category) {
|
||||
final first = category.items.isNotEmpty ? category.items.first : null;
|
||||
return '{id=${category.id}, icon=${category.iconResource}, '
|
||||
'count=${category.items.length}, firstDisplay=${first?.displayResource}, '
|
||||
'firstSend=${first?.sendResource}}';
|
||||
})
|
||||
.join(', ');
|
||||
final String iconAsset;
|
||||
final List<String> assets;
|
||||
}
|
||||
|
||||
class PopRoute extends PopupRoute {
|
||||
|
||||
@ -1587,12 +1587,19 @@ class _MsgItemState extends State<MsgItem> {
|
||||
if (path.isEmpty) {
|
||||
return SizedBox(width: width, height: height);
|
||||
}
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
if (_isLocalAsset(path)) {
|
||||
return RoomEmojiAssetImage(
|
||||
key: ValueKey(path),
|
||||
asset: path,
|
||||
width: width,
|
||||
height: height,
|
||||
);
|
||||
}
|
||||
return netImage(url: path, width: width, height: height);
|
||||
}
|
||||
|
||||
bool _isLocalAsset(String path) {
|
||||
return path.startsWith("assets/") || path.startsWith("sc_images/");
|
||||
}
|
||||
|
||||
Widget _buildDiceMsg(BuildContext context) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svga/flutter_svga.dart';
|
||||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||||
@ -51,13 +50,8 @@ class SCNetworkSvgaWidget extends StatefulWidget {
|
||||
|
||||
class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static final Set<String> _loggedLoadingResources = <String>{};
|
||||
static final Set<String> _loggedReadyResources = <String>{};
|
||||
static final Set<String> _loggedErrorResources = <String>{};
|
||||
|
||||
late final SVGAAnimationController _controller;
|
||||
String? _loadedResource;
|
||||
String? _retainedResource;
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
@ -83,7 +77,6 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
Future<void> _loadResource() async {
|
||||
final resource = widget.resource.trim();
|
||||
if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) {
|
||||
_releaseRetainedResource();
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
_loadedResource = null;
|
||||
@ -97,45 +90,27 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
});
|
||||
|
||||
try {
|
||||
_debugSvgaOnce(_loggedLoadingResources, 'loading', resource);
|
||||
final manager = SCGiftVapSvgaManager();
|
||||
final shouldRetain = _retainedResource != resource;
|
||||
var movieEntity = manager.getCachedSvgaEntity(
|
||||
resource,
|
||||
retain: shouldRetain,
|
||||
);
|
||||
var retained = movieEntity != null && shouldRetain;
|
||||
final cache = SCGiftVapSvgaManager().videoItemCache;
|
||||
var movieEntity = cache[resource];
|
||||
if (movieEntity == null) {
|
||||
movieEntity =
|
||||
resource.startsWith("http")
|
||||
? await SVGAParser.shared.decodeFromURL(resource)
|
||||
: await SVGAParser.shared.decodeFromAssets(resource);
|
||||
movieEntity.autorelease = false;
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
movieEntity.dispose();
|
||||
return;
|
||||
}
|
||||
manager.cacheSvgaEntity(resource, movieEntity, retain: shouldRetain);
|
||||
retained = shouldRetain;
|
||||
cache[resource] = movieEntity;
|
||||
}
|
||||
if (widget.movieConfigurer != null) {
|
||||
movieEntity.dynamicItem.reset();
|
||||
await widget.movieConfigurer!(movieEntity);
|
||||
}
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
if (retained) {
|
||||
manager.releaseCachedSvgaEntity(resource);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (retained) {
|
||||
_replaceRetainedResource(resource);
|
||||
}
|
||||
setState(() {
|
||||
_loadedResource = resource;
|
||||
_controller.videoItem = movieEntity;
|
||||
});
|
||||
_debugSvgaOnce(_loggedReadyResources, 'ready', resource);
|
||||
_syncPlayback(restartIfActive: true);
|
||||
} catch (error) {
|
||||
if (!mounted || widget.resource.trim() != resource) {
|
||||
@ -146,17 +121,9 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
_loadedResource = null;
|
||||
_controller.videoItem = null;
|
||||
});
|
||||
_debugSvgaOnce(_loggedErrorResources, 'failed error=$error', resource);
|
||||
}
|
||||
}
|
||||
|
||||
void _debugSvgaOnce(Set<String> bucket, String message, String resource) {
|
||||
if (!kDebugMode || !bucket.add('$message|$resource')) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[NetworkSvga] $message resource=$resource');
|
||||
}
|
||||
|
||||
void _syncPlayback({bool restartIfActive = false}) {
|
||||
if (!widget.active || _controller.videoItem == null) {
|
||||
_controller.stop();
|
||||
@ -175,29 +142,8 @@ class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
|
||||
}
|
||||
}
|
||||
|
||||
void _replaceRetainedResource(String resource) {
|
||||
final previous = _retainedResource;
|
||||
if (previous == resource) {
|
||||
return;
|
||||
}
|
||||
if (previous != null) {
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(previous);
|
||||
}
|
||||
_retainedResource = resource;
|
||||
}
|
||||
|
||||
void _releaseRetainedResource() {
|
||||
final resource = _retainedResource;
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
_retainedResource = null;
|
||||
SCGiftVapSvgaManager().releaseCachedSvgaEntity(resource);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_releaseRetainedResource();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -42,6 +42,6 @@ android {
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation "com.tencent.tav:libpag:4.5.70"
|
||||
implementation "com.tencent.tav:libpag:4.3.68"
|
||||
implementation 'com.jakewharton:disklrucache:2.0.2'
|
||||
}
|
||||
|
||||
163
pubspec.yaml
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 1.6.3+1631
|
||||
version: 1.6.3+1631
|
||||
|
||||
|
||||
environment:
|
||||
@ -47,19 +47,19 @@ dependencies:
|
||||
pretty_dio_logger: ^1.1.1
|
||||
#下拉刷新
|
||||
pull_to_refresh: 2.0.0
|
||||
#腾讯TRTC
|
||||
tencent_rtc_sdk: ^13.2.2
|
||||
#腾讯TRTC
|
||||
tencent_rtc_sdk: ^13.2.2
|
||||
#腾讯IM
|
||||
tencent_cloud_chat_sdk: 8.3.6498+3
|
||||
#缓存
|
||||
flutter_cache_manager: ^3.0.0
|
||||
#路由
|
||||
fluro: 2.0.5
|
||||
#权限请求
|
||||
permission_handler: ^12.0.0+1
|
||||
webview_flutter: 4.4.2
|
||||
#跳转外链
|
||||
url_launcher: ^6.3.1
|
||||
#权限请求
|
||||
permission_handler: ^12.0.0+1
|
||||
webview_flutter: 4.4.2
|
||||
#跳转外链
|
||||
url_launcher: ^6.3.1
|
||||
#键值对存储
|
||||
# mmkv: ^2.2.2
|
||||
shared_preferences: ^2.5.3
|
||||
@ -84,26 +84,26 @@ dependencies:
|
||||
#相册
|
||||
# wechat_assets_picker: ^9.5.0
|
||||
# wechat_camera_picker: ^4.2.0
|
||||
image_picker: ^1.2.0
|
||||
#图片裁剪
|
||||
image_cropper:
|
||||
path: ./local_packages/image_cropper-5.0.1-patched
|
||||
back_button_interceptor: ^8.0.4
|
||||
image_picker: ^1.2.0
|
||||
#图片裁剪
|
||||
image_cropper:
|
||||
path: ./local_packages/image_cropper-5.0.1-patched
|
||||
back_button_interceptor: ^8.0.4
|
||||
#vap特效
|
||||
# flutter_vap_plus: ^1.2.10
|
||||
# tancent_vap: ^1.0.0+1
|
||||
tancent_vap:
|
||||
path: ./local_packages/tancent_vap-1.0.0+1
|
||||
#svga特效
|
||||
flutter_svga:
|
||||
path: ./local_packages/flutter_svga-0.0.13-patched
|
||||
#banner
|
||||
tancent_vap:
|
||||
path: ./local_packages/tancent_vap-1.0.0+1
|
||||
#svga特效
|
||||
flutter_svga:
|
||||
path: ./local_packages/flutter_svga-0.0.13-patched
|
||||
#banner
|
||||
carousel_slider: ^5.1.1
|
||||
#谷歌支付
|
||||
in_app_purchase: ^3.2.3
|
||||
in_app_purchase_android: ^0.4.0+10
|
||||
|
||||
device_info_plus: ^10.0.0
|
||||
#谷歌支付
|
||||
in_app_purchase: ^3.2.3
|
||||
in_app_purchase_android: ^0.4.0+10
|
||||
|
||||
device_info_plus: ^10.0.0
|
||||
package_info_plus: ^8.0.0
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.1.0
|
||||
@ -115,44 +115,44 @@ dependencies:
|
||||
# flutter_sound: 9.28.0
|
||||
#图片压缩
|
||||
flutter_image_compress: ^2.4.0
|
||||
extended_text: ^15.0.2
|
||||
audioplayers: ^6.5.1
|
||||
#缩略图
|
||||
video_thumbnail: ^0.5.6
|
||||
extended_text: ^15.0.2
|
||||
audioplayers: ^6.5.1
|
||||
#缩略图
|
||||
video_thumbnail: ^0.5.6
|
||||
badges: ^3.1.2
|
||||
#本地路径
|
||||
path_provider: ^2.1.4
|
||||
path: ^1.9.0
|
||||
on_audio_query:
|
||||
path: ./local_packages/on_audio_query-2.9.0
|
||||
photo_view: ^0.15.0
|
||||
#本地路径
|
||||
path_provider: ^2.1.4
|
||||
path: ^1.9.0
|
||||
on_audio_query:
|
||||
path: ./local_packages/on_audio_query-2.9.0
|
||||
photo_view: ^0.15.0
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
marquee: ^2.3.0
|
||||
extended_nested_scroll_view: ^6.2.1
|
||||
event_bus: ^2.0.1
|
||||
readmore: ^3.0.0
|
||||
#外链跳转
|
||||
app_links: ^6.4.1
|
||||
readmore: ^3.0.0
|
||||
#外链跳转
|
||||
app_links: ^6.4.1
|
||||
flutter_svg: ^2.3.0
|
||||
pag:
|
||||
path: ./local_packages/pag-1.0.7-patched
|
||||
pag:
|
||||
path: ./local_packages/pag-1.0.7-patched
|
||||
just_audio: ^0.10.5
|
||||
audio_session: ^0.2.3
|
||||
file_picker: ^11.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
dependency_overrides:
|
||||
# path_provider_foundation 2.6.0 pulls in objective_c native assets that are
|
||||
# currently packaged as simulator frameworks and rejected by App Store upload.
|
||||
path_provider_foundation: 2.4.2
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
dev_dependencies:
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
dependency_overrides:
|
||||
# path_provider_foundation 2.6.0 pulls in objective_c native assets that are
|
||||
# currently packaged as simulator frameworks and rejected by App Store upload.
|
||||
path_provider_foundation: 2.4.2
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
@ -164,37 +164,38 @@ flutter:
|
||||
# - family: MyCustomFont
|
||||
# fonts:
|
||||
# - asset: fonts/AAA-bold.ttf
|
||||
assets:
|
||||
# 共享资源
|
||||
# 应用资源路径
|
||||
- assets/
|
||||
- assets/debug/
|
||||
- assets/l10n/
|
||||
- sc_images/splash/
|
||||
assets:
|
||||
# 共享资源
|
||||
# 应用资源路径
|
||||
- assets/
|
||||
- assets/debug/
|
||||
- assets/l10n/
|
||||
- sc_images/splash/
|
||||
- sc_images/login/
|
||||
- sc_images/general/
|
||||
- sc_images/index/
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/first_recharge/
|
||||
- sc_images/room/
|
||||
- sc_images/room/cp_profile/
|
||||
- sc_images/room/cp_rights/
|
||||
- sc_images/room/cp_progress/
|
||||
- sc_images/room/cp_invite/
|
||||
- sc_images/room/background_examples/
|
||||
- sc_images/room/cp_broadcast/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/mic/
|
||||
- sc_images/room/red_packet/
|
||||
- sc_images/room/rocket/
|
||||
- sc_images/room/anim/
|
||||
- sc_images/room/anim/gift/
|
||||
- sc_images/room/anim/luck_gift/
|
||||
- sc_images/person/
|
||||
- sc_images/general/
|
||||
- sc_images/index/
|
||||
- sc_images/daily_sign_in/
|
||||
- sc_images/register_reward/
|
||||
- sc_images/first_recharge/
|
||||
- sc_images/room/
|
||||
- sc_images/room/cp_profile/
|
||||
- sc_images/room/cp_rights/
|
||||
- sc_images/room/cp_progress/
|
||||
- sc_images/room/cp_invite/
|
||||
- sc_images/room/background_examples/
|
||||
- sc_images/room/cp_broadcast/
|
||||
- sc_images/room/emoji/
|
||||
- sc_images/room/entrance/
|
||||
- sc_images/room/mic/
|
||||
- sc_images/room/red_packet/
|
||||
- sc_images/room/rocket/
|
||||
- sc_images/room/anim/
|
||||
- sc_images/room/anim/gift/
|
||||
- sc_images/room/anim/luck_gift/
|
||||
- sc_images/person/
|
||||
- sc_images/store/
|
||||
- sc_images/msg/
|
||||
- sc_images/level/
|
||||
- sc_images/coupon/
|
||||
- sc_images/vip/
|
||||
- fonts/
|
||||
- fonts/
|
||||
|
||||
BIN
sc_images/room/emoji/fluent_emoji_01.gif
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
sc_images/room/emoji/fluent_emoji_01.webp
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
sc_images/room/emoji/fluent_emoji_02.gif
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
sc_images/room/emoji/fluent_emoji_02.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
sc_images/room/emoji/fluent_emoji_03.gif
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
sc_images/room/emoji/fluent_emoji_03.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
sc_images/room/emoji/fluent_emoji_04.gif
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
sc_images/room/emoji/fluent_emoji_04.webp
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
sc_images/room/emoji/fluent_emoji_05.gif
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
sc_images/room/emoji/fluent_emoji_05.webp
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
sc_images/room/emoji/fluent_emoji_06.gif
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
sc_images/room/emoji/fluent_emoji_06.webp
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
sc_images/room/emoji/fluent_emoji_07.gif
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
sc_images/room/emoji/fluent_emoji_07.webp
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
sc_images/room/emoji/fluent_emoji_08.gif
Normal file
|
After Width: | Height: | Size: 299 KiB |
BIN
sc_images/room/emoji/fluent_emoji_08.webp
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
sc_images/room/emoji/fluent_emoji_09.gif
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
sc_images/room/emoji/fluent_emoji_09.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
sc_images/room/emoji/fluent_emoji_10.gif
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
sc_images/room/emoji/fluent_emoji_10.webp
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
sc_images/room/emoji/monkey_1.gif
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
sc_images/room/emoji/monkey_10.gif
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
sc_images/room/emoji/monkey_2.gif
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
sc_images/room/emoji/monkey_3.gif
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
sc_images/room/emoji/monkey_4.gif
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
sc_images/room/emoji/monkey_5.gif
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
sc_images/room/emoji/monkey_6.gif
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
sc_images/room/emoji/monkey_7.gif
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
sc_images/room/emoji/monkey_8.gif
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
sc_images/room/emoji/monkey_9.gif
Normal file
|
After Width: | Height: | Size: 79 KiB |
@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/modules/room_game/views/baishun_loading_view.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('room game loading overlay can be placed inside stack', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(375, 812),
|
||||
builder:
|
||||
(_, __) => const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
ColoredBox(color: Colors.black),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(child: BaishunLoadingView()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byType(BaishunLoadingView), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@ -44,33 +44,6 @@ void main() {
|
||||
expect(debugRoomRocketPagSourceKind('assets/rocket/rocket.pag'), 'asset');
|
||||
});
|
||||
|
||||
test('uses native iOS pag view for loaded bytes and assets', () {
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: true,
|
||||
hasAsset: false,
|
||||
hasBytes: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: true,
|
||||
hasAsset: true,
|
||||
hasBytes: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
debugShouldUseNativeRoomRocketPagView(
|
||||
isIOS: false,
|
||||
hasAsset: true,
|
||||
hasBytes: true,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps pag bytes in memory for five minutes by default', () {
|
||||
expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5));
|
||||
});
|
||||
|
||||
@ -21,7 +21,7 @@ void main() {
|
||||
};
|
||||
final msg = Msg(
|
||||
groupId: 'room-1',
|
||||
msg: 'https://cdn.example.com/emoji/fluent_emoji_01.webp',
|
||||
msg: 'sc_images/room/emoji/fluent_emoji_01.webp',
|
||||
type: SCRoomMsgType.emoticons,
|
||||
number: 1,
|
||||
);
|
||||
|
||||
@ -3,8 +3,6 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_mic_go_up_res.dart'
|
||||
as mic_go_up;
|
||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||
|
||||
void main() {
|
||||
@ -32,112 +30,4 @@ void main() {
|
||||
expect(rtcProvider.roomWheatMap[3]?.user?.id, 'user-1');
|
||||
expect(rtcProvider.isOnMai(), isTrue);
|
||||
});
|
||||
|
||||
test('self mic detection matches account when mic user id is missing', () {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
AccountStorage().setCurrentUser(
|
||||
SocialChatLoginRes(
|
||||
userProfile: SocialChatUserProfile(
|
||||
id: 'user-1',
|
||||
account: '8826517',
|
||||
userNickname: 'Me',
|
||||
),
|
||||
),
|
||||
);
|
||||
final rtcProvider =
|
||||
RealTimeCommunicationManager()
|
||||
..roomWheatMap = {
|
||||
2: MicRes(
|
||||
micIndex: 2,
|
||||
user: SocialChatUserProfile(account: '8826517'),
|
||||
),
|
||||
};
|
||||
|
||||
expect(rtcProvider.isOnMai(), isTrue);
|
||||
expect(rtcProvider.isOnMaiInIndex(2), isTrue);
|
||||
expect(rtcProvider.userOnMaiInIndex('user-1'), 2);
|
||||
});
|
||||
|
||||
test('self go-up uses response user when local profile is unavailable', () {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
final rtcProvider =
|
||||
RealTimeCommunicationManager()..roomWheatMap = {4: MicRes(micIndex: 4)};
|
||||
|
||||
rtcProvider.debugApplySelfMicGoUpResponse(
|
||||
targetIndex: 4,
|
||||
micGoUpRes: mic_go_up.SCMicGoUpRes(
|
||||
micIndex: 4,
|
||||
micMute: false,
|
||||
micLock: false,
|
||||
roomToken: 'token',
|
||||
user: mic_go_up.User(
|
||||
id: 'user-android',
|
||||
account: 'android-account',
|
||||
userNickname: 'Android User',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rtcProvider.isOnMai(), isTrue);
|
||||
expect(rtcProvider.isOnMaiInIndex(4), isTrue);
|
||||
expect(rtcProvider.roomWheatMap[4]?.user?.id, 'user-android');
|
||||
expect(rtcProvider.roomWheatMap[4]?.user?.account, 'android-account');
|
||||
});
|
||||
|
||||
test('self go-up prefers response identity over stale local profile', () {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
AccountStorage().setCurrentUser(
|
||||
SocialChatLoginRes(
|
||||
userProfile: SocialChatUserProfile(
|
||||
id: 'stale-local-id',
|
||||
account: 'stale-local-account',
|
||||
userNickname: 'Me',
|
||||
),
|
||||
),
|
||||
);
|
||||
final rtcProvider =
|
||||
RealTimeCommunicationManager()..roomWheatMap = {5: MicRes(micIndex: 5)};
|
||||
|
||||
rtcProvider.debugApplySelfMicGoUpResponse(
|
||||
localUser: AccountStorage().getCurrentUser()?.userProfile,
|
||||
targetIndex: 5,
|
||||
micGoUpRes: mic_go_up.SCMicGoUpRes(
|
||||
micIndex: 5,
|
||||
micMute: false,
|
||||
user: mic_go_up.User(
|
||||
id: 'server-seat-id',
|
||||
account: 'server-seat-account',
|
||||
userNickname: 'Server User',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(rtcProvider.isOnMai(), isTrue);
|
||||
expect(rtcProvider.isOnMaiInIndex(5), isTrue);
|
||||
expect(rtcProvider.roomWheatMap[5]?.user?.id, 'server-seat-id');
|
||||
expect(rtcProvider.roomWheatMap[5]?.user?.account, 'server-seat-account');
|
||||
});
|
||||
|
||||
test('self mic state follows microphone list snapshots', () {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
AccountStorage().setCurrentUser(
|
||||
SocialChatLoginRes(
|
||||
userProfile: SocialChatUserProfile(id: 'user-1', userNickname: 'Me'),
|
||||
),
|
||||
);
|
||||
final rtcProvider = RealTimeCommunicationManager();
|
||||
|
||||
rtcProvider.debugApplyMicListSnapshot([
|
||||
MicRes(
|
||||
micIndex: 2,
|
||||
user: SocialChatUserProfile(id: 'user-1', userNickname: 'Me'),
|
||||
),
|
||||
], notifyIfUnchanged: false);
|
||||
expect(rtcProvider.isOnMai(), isTrue);
|
||||
|
||||
rtcProvider.debugApplyMicListSnapshot([
|
||||
MicRes(micIndex: 2),
|
||||
], notifyIfUnchanged: false);
|
||||
expect(rtcProvider.isOnMai(), isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,34 +0,0 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
|
||||
|
||||
void main() {
|
||||
group('SCRoomEmojiRes', () {
|
||||
test('parses backend emoji group fields defensively', () {
|
||||
final group = SCRoomEmojiRes.fromJson({
|
||||
'id': 1001,
|
||||
'sysOrigin': 'LIKEI',
|
||||
'groupCode': 'DEFAULT',
|
||||
'groupName': 'Default',
|
||||
'cover': 'https://cdn.example.com/emoji/default-cover.png',
|
||||
'have': 'true',
|
||||
'amount': '9.9',
|
||||
'emojis': [
|
||||
{
|
||||
'sourceUrl': 'https://cdn.example.com/emoji/hello.svga',
|
||||
'sourceType': '1',
|
||||
'coverUrl': 'https://cdn.example.com/emoji/hello.png',
|
||||
'type': 'NORMAL',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(group.id, '1001');
|
||||
expect(group.have, isTrue);
|
||||
expect(group.amount, 9.9);
|
||||
expect(group.emojis, hasLength(1));
|
||||
expect(group.emojis!.first.sourceType, 1);
|
||||
expect(group.emojis!.first.previewUrl, contains('hello.png'));
|
||||
expect(group.emojis!.first.sendUrl, contains('hello.svga'));
|
||||
});
|
||||
});
|
||||
}
|
||||
8
需求进度.md
@ -1070,11 +1070,3 @@
|
||||
- [ ] 16:41:11 开始格式化并静态检查首充类型识别与兜底图标改动。
|
||||
|
||||
- [x] 16:41:33 类型识别与兜底图标改动已通过 dart format 和 flutter analyze。
|
||||
|
||||
- [ ] 2026-06-02 房间表情反馈:公屏 GIF 不循环,表情弹窗格子应展示动画预览而不是静态封面;开始定位面板资源字段和公屏渲染组件。
|
||||
- [x] 2026-06-02 已定位:表情弹窗格子当前使用 `displayResource/coverUrl`,发送与公屏使用 `sendResource/sourceUrl`;若 cover 为 PNG 会导致面板静态,公屏 GIF 需要在首次加载完成后主动重建来重播有限循环资源。
|
||||
- [x] 2026-06-02 按最新反馈收窄范围:表情面板继续使用 `displayResource` 不改;仅调整 GIF 媒体组件在首次加载完成后重建播放,修复公屏 GIF 停在最后一帧。
|
||||
- [x] 2026-06-02 继续优化公屏 GIF 循环闪 loading:网络 GIF 重播时不再销毁整个 widget,加载下一轮期间复用上一帧画面,避免循环间隙出现转圈。
|
||||
- [x] 2026-06-02 复核发现不销毁 key 会导致 GIF 解码器不重启并停在最后一帧;改为前景层用新 key 重播、背景层保留上一轮画面,兼顾循环播放和不显示 loading。
|
||||
- [x] 2026-06-02 按接口文档恢复 `have` 权限判断:`/material/emoji/all` 返回的表情包仅 `have == true` 且非空时展示,`have=false/null` 计入 `filteredNoAccess` 日志。
|
||||
- [x] 2026-06-02 排查 Android 表情包一直转圈:初步判断不是接口解析,表情组已进入渲染后媒体加载失败态被转圈 fallback 掩盖;表情图片/GIF 按项目通用 `netImage` 补解码尺寸、失败清缓存,并把失败 fallback 改为静态占位便于区分 loading 与 failed。
|
||||
|
||||