diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index f1ef236..aa4da82 100755 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -6,6 +6,7 @@ "codeSent": "A 6-digit code has been sent to {account}. Please enter it within 30 minutes.", "back": "Back", "enterEmailOrPhone": "Enter Email or Phone Number", + "enterEmail": "Please enter email address", "enterCode": "Please enter verification code", "enterPassword": "Please enter password", "reenterPassword": "Please re-enter password", diff --git a/assets/translations/strings_es.i18n.json b/assets/translations/strings_es.i18n.json index a48764e..8aca355 100755 --- a/assets/translations/strings_es.i18n.json +++ b/assets/translations/strings_es.i18n.json @@ -6,6 +6,7 @@ "codeSent": "Se ha enviado un código de 6 dígitos a {account}. Por favor, ingrésalo en los próximos 30 minutos.", "back": "Atrás", "enterEmailOrPhone": "Ingresa correo o teléfono", + "enterEmail": "Please enter email address", "enterCode": "Por favor, ingresa el código de verificación", "enterPassword": "Por favor, ingresa la contraseña", "reenterPassword": "Por favor, reingresa la contraseña", diff --git a/assets/translations/strings_es.i18n.json.bak b/assets/translations/strings_es.i18n.json.bak new file mode 100755 index 0000000..a48764e --- /dev/null +++ b/assets/translations/strings_es.i18n.json.bak @@ -0,0 +1,441 @@ +{ + "login": { + "welcome": "¡Bienvenido a BearVPN!", + "verifyPhone": "Verifica tu número de teléfono", + "verifyEmail": "Verifica tu correo electrónico", + "codeSent": "Se ha enviado un código de 6 dígitos a {account}. Por favor, ingrésalo en los próximos 30 minutos.", + "back": "Atrás", + "enterEmailOrPhone": "Ingresa correo o teléfono", + "enterCode": "Por favor, ingresa el código de verificación", + "enterPassword": "Por favor, ingresa la contraseña", + "reenterPassword": "Por favor, reingresa la contraseña", + "forgotPassword": "Olvidé mi contraseña", + "codeLogin": "Iniciar sesión con código", + "passwordLogin": "Iniciar sesión con contraseña", + "agreeTerms": "Iniciar sesión/Crear cuenta, acepto", + "termsOfService": "Términos de servicio", + "privacyPolicy": "Política de privacidad", + "next": "Siguiente", + "registerNow": "Registrarse ahora", + "setAndLogin": "Configurar e iniciar sesión", + "enterAccount": "Por favor, ingresa la cuenta", + "passwordMismatch": "Las dos contraseñas no coinciden", + "sendCode": "Enviar código", + "codeSentCountdown": "Código enviado {seconds}s", + "and": "y", + "enterInviteCode": "Ingresa código de invitación (opcional)", + "registerSuccess": "Registro exitoso" + }, + "failure": { + "unexpected": "Error inesperado", + "clash": { + "unexpected": "Error inesperado", + "core": "Error de Clash ${reason}" + }, + "singbox": { + "unexpected": "Error de servicio inesperado", + "serviceNotRunning": "Servicio no en ejecución", + "missingPrivilege": "Privilegios insuficientes", + "missingPrivilegeMsg": "El modo VPN requiere privilegios de administrador. Reinicia la aplicación como administrador o cambia el modo de servicio", + "missingGeoAssets": "Faltan recursos GEO", + "missingGeoAssetsMsg": "Faltan archivos de recursos GEO. Considera cambiar los recursos activos o descarga los recursos seleccionados en configuración.", + "invalidConfigOptions": "Opciones de configuración inválidas", + "invalidConfig": "Configuración inválida", + "create": "Error al crear servicio", + "start": "Error al iniciar servicio" + }, + "connectivity": { + "unexpected": "Fallo inesperado", + "missingVpnPermission": "Falta permiso VPN", + "missingNotificationPermission": "Falta permiso de notificaciones", + "core": "Error del núcleo" + }, + "profiles": { + "unexpected": "Error inesperado", + "notFound": "Perfil no encontrado", + "invalidConfig": "Configuración inválida", + "invalidUrl": "URL inválida" + }, + "connection": { + "unexpected": "Error de conexión inesperado", + "timeout": "Tiempo de conexión agotado", + "badResponse": "Respuesta incorrecta", + "connectionError": "Error de conexión", + "badCertificate": "Certificado inválido" + }, + "geoAssets": { + "unexpected": "Error inesperado", + "notUpdate": "No hay actualizaciones disponibles", + "activeNotFound": "No se encontraron recursos GEO activos" + } + }, + "userInfo": { + "title": "Mi información", + "bindingTip": "Correo/teléfono no vinculado", + "myAccount": "Mi cuenta", + "balance": "Saldo", + "noValidSubscription": "No tiene una suscripción válida", + "subscribeNow": "Suscribirse ahora", + "shortcuts": "Accesos directos", + "adBlock": "Bloqueo de anuncios", + "dnsUnlock": "Desbloqueo DNS", + "contactUs": "Contáctanos", + "others": "Otros", + "logout": "Cerrar sesión", + "logoutConfirmTitle": "Cerrar sesión", + "logoutConfirmMessage": "¿Estás seguro de que quieres cerrar sesión?", + "logoutCancel": "Cancelar", + "vpnWebsite": "Sitio web VPN", + "telegram": "Telegram", + "mail": "Correo", + "phone": "Teléfono", + "customerService": "Servicio al cliente", + "workOrder": "Enviar ticket", + "pleaseLogin": "Por favor, inicia sesión primero", + "subscriptionValid": "Suscripción válida", + "startTime": "Fecha de inicio:", + "expireTime": "Fecha de vencimiento:", + "loginNow": "Iniciar sesión ahora", + "trialPeriod": "Bienvenido a la prueba Premium", + "remainingTime": "Tiempo restante", + "trialExpired": "Prueba expirada, conexión desconectada", + "subscriptionExpired": "Suscripción expirada, conexión desconectada", + "copySuccess": "Copiado con éxito", + "notAvailable": "No disponible", + "deviceLimit": "Límite de dispositivos: {count}", + "reset": "Restablecer", + "trafficUsage": "Usado: {used} / {total}", + "trafficProgress": { + "title": "Uso de tráfico", + "unlimited": "Tráfico ilimitado", + "limited": "Tráfico usado" + }, + "switchSubscription": "Cambiar Suscripción", + "resetTrafficTitle": "Restablecer Tráfico", + "resetTrafficMessage": "Ejemplo de restablecimiento de tráfico del plan mensual: restablecer el tráfico del siguiente ciclo mensualmente, y el período de validez de la suscripción se adelantará de {currentTime} a {newTime}", + "trialStatus": "Estado de prueba", + "trialing": "En período de prueba", + "trialEndMessage": "No podrá continuar usando después de que expire el período de prueba", + "lastDaySubscriptionStatus": "Suscripción por expirar", + "lastDaySubscriptionMessage": "Por expirar", + "subscriptionEndMessage": "No podrá continuar usando después de que expire la suscripción", + "trialTimeWithDays": "{days}d {hours}h {minutes}m {seconds}s", + "trialTimeWithHours": "{hours}h {minutes}m {seconds}s", + "trialTimeWithMinutes": "{minutes}m {seconds}s", + "refreshLatency": "Actualizar latencia", + "testLatency": "Probar latencia", + "testing": "Probando latencia", + "refreshLatencyDesc": "Actualizar latencia de todos los nodos", + "testAllNodesLatency": "Probar la latencia de red de todos los nodos", + "autoSelect": "Selección automática", + "selected": "Seleccionado", + "willBeDeleted": "será eliminado", + "deleteAccountWarning": "La eliminación de la cuenta es permanente. Una vez que se elimine su cuenta, no podrá utilizar ninguna función. ¿Continuar?", + "requestDelete": "Solicitar eliminación" + }, + "setting": { + "title": "Configuración", + "vpnConnection": "Conexión VPN", + "general": "General", + "autoConnect": "Conexión automática", + "routeRule": "Reglas de ruta", + "countrySelector": "Seleccionar país", + "appearance": "Apariencia", + "notifications": "Notificaciones", + "helpImprove": "Ayúdanos a mejorar", + "helpImproveSubtitle": "Subtítulo de ayuda para mejorar", + "requestDeleteAccount": "Solicitar eliminación de cuenta", + "goToDelete": "Ir a eliminar", + "rateUs": "Califícanos en App Store", + "iosRating": "Calificación iOS", + "version": "Versión", + "switchLanguage": "Cambiar idioma", + "system": "Sistema", + "light": "Claro", + "dark": "Oscuro", + "vpnModeSmart": "Modo inteligente", + "mode": "Modo de salida", + "connectionTypeGlobal": "Proxy global", + "connectionTypeGlobalRemark": "Cuando está activado, todo el tráfico pasa por el proxy", + "connectionTypeRule": "Proxy inteligente", + "connectionTypeRuleRemark": "Cuando el [Modo de salida] está configurado en [Proxy inteligente], el sistema dividirá automáticamente el tráfico nacional e internacional según el país seleccionado: las IPs/dominios nacionales se conectan directamente, mientras que las solicitudes extranjeras se acceden a través del proxy", + "connectionTypeDirect": "Conexión directa", + "connectionTypeDirectRemark": "Cuando está activado, todo el tráfico evita el proxy", + "smartMode": "Modo inteligente", + "secureMode": "Modo seguro" + }, + "statistics": { + "title": "Estadísticas", + "vpnStatus": "Estado VPN", + "ipAddress": "Dirección IP", + "connectionTime": "Tiempo de conexión", + "protocol": "Protocolo", + "weeklyProtectionTime": "Tiempo de protección semanal", + "currentStreak": "Racha actual", + "highestStreak": "Mejor racha", + "longestConnection": "Conexión más larga", + "days": "{days} días", + "daysOfWeek": { + "monday": "Lun", + "tuesday": "Mar", + "wednesday": "Mié", + "thursday": "Jue", + "friday": "Vie", + "saturday": "Sáb", + "sunday": "Dom" + } + }, + "message": { + "title": "Notificaciones", + "system": "Mensajes del sistema", + "promotion": "Mensajes promocionales" + }, + "home": { + "welcome": "Bienvenido a BearVPN", + "disconnected": "Desconectado", + "connecting": "Conectando", + "connected": "Conectado", + "disconnecting": "Desconectando", + "currentConnectionTitle": "Conexión actual", + "switchNode": "Cambiar nodo", + "timeout": "Tiempo de espera agotado", + "loading": "Cargando...", + "error": "Error de carga", + "checkNetwork": "Verifique su conexión de red e intente nuevamente", + "retry": "Reintentar", + "connectionSectionTitle": "Método de conexión", + "dedicatedServers": "Servidores dedicados", + "countryRegion": "País/Región", + "serverListTitle": "Grupos de servidores dedicados", + "nodeListTitle": "Todos los nodos", + "countryListTitle": "Lista de países/regiones", + "noServers": "No hay servidores disponibles", + "noNodes": "No hay nodos disponibles", + "noRegions": "No hay regiones disponibles", + "subscriptionDescription": "Obtenga acceso premium a la red global de alta velocidad", + "subscribe": "Suscribirse", + "trialPeriod": "Bienvenido a la versión de prueba Premium", + "remainingTime": "Tiempo restante", + "trialExpired": "Período de prueba expirado, conexión terminada", + "subscriptionExpired": "Suscripción expirada, conexión terminada", + "subscriptionUpdated": "Suscripción actualizada", + "subscriptionUpdatedMessage": "Su información de suscripción ha sido actualizada, actualice para ver el último estado", + "trialStatus": "Estado de prueba", + "trialing": "En período de prueba", + "trialEndMessage": "No podrá continuar usando después de que expire el período de prueba", + "lastDaySubscriptionStatus": "Suscripción por expirar", + "lastDaySubscriptionMessage": "Por expirar", + "subscriptionEndMessage": "No podrá continuar usando después de que expire la suscripción", + "trialTimeWithDays": "{days}d {hours}h {minutes}m {seconds}s", + "trialTimeWithHours": "{hours}h {minutes}m {seconds}s", + "trialTimeWithMinutes": "{minutes}m {seconds}s", + "refreshLatency": "Actualizar latencia", + "testLatency": "Probar latencia", + "testing": "Probando latencia", + "refreshLatencyDesc": "Actualizar latencia de todos los nodos", + "testAllNodesLatency": "Probar la latencia de red de todos los nodos", + "autoSelect": "Selección automática", + "selected": "Seleccionado" + }, + "invite": { + "title": "Invitar amigos", + "progress": "Progreso de invitación", + "inviteStats": "Estadísticas de invitación", + "registers": "Registrados", + "totalCommission": "Comisión total", + "rewardDetails": "Detalles de recompensa >", + "steps": "Pasos de invitación", + "inviteFriend": "Invitar amigo", + "acceptInvite": "El amigo acepta la invitación\ny se registra", + "getReward": "Obtener recompensa", + "shareLink": "Compartir por enlace", + "shareQR": "Compartir por código QR", + "rules": "Reglas de invitación", + "rule1": "1. Puedes invitar amigos compartiendo tu enlace o código de invitación exclusivo.", + "rule2": "2. Después de que tu amigo complete el registro e inicie sesión, la recompensa por invitación se enviará automáticamente a tu cuenta.", + "pending": "Pendiente de descarga", + "processing": "En proceso", + "success": "Exitoso", + "expired": "Expirado", + "myInviteCode": "Mi código de invitación", + "inviteCodeCopied": "Código de invitación copiado al portapapeles" + }, + "purchaseMembership": { + "purchasePackage": "Comprar Paquete", + "noData": "No hay paquetes disponibles", + "myAccount": "Mi Cuenta", + "selectPackage": "Seleccionar Paquete", + "packageDescription": "Descripción del Paquete", + "paymentMethod": "Método de Pago", + "cancelAnytime": "Puedes cancelar en cualquier momento en la APP", + "startSubscription": "Comenzar Suscripción", + "renewNow": "Renovar Ahora", + "month": "{quantity} meses", + "year": "{quantity} años", + "day": "{quantity} días", + "unlimitedTraffic": "Tráfico Ilimitado", + "unlimitedDevices": "Dispositivos Ilimitados", + "devices": "{count} dispositivos", + "features": "Características del Paquete", + "expand": "Expandir", + "collapse": "Colapsar", + "confirmPurchase": "Confirmar Compra", + "confirmPurchaseDesc": "¿Está seguro de que desea comprar este paquete?" + }, + "orderStatus": { + "title": "Estado del Pedido", + "pending": { + "title": "Pago Pendiente", + "description": "Por favor complete el pago" + }, + "paid": { + "title": "Pago Recibido", + "description": "Procesando su pedido" + }, + "success": { + "title": "¡Felicitaciones! Pago Exitoso", + "description": "Su paquete ha sido comprado exitosamente" + }, + "closed": { + "title": "Pedido Cerrado", + "description": "Por favor realice un nuevo pedido" + }, + "failed": { + "title": "Pago Fallido", + "description": "Por favor intente el pago nuevamente" + }, + "unknown": { + "title": "Estado Desconocido", + "description": "Por favor contacte al servicio al cliente" + }, + "checkFailed": { + "title": "Verificación Fallida", + "description": "Por favor intente nuevamente más tarde" + }, + "initial": { + "title": "Procesando Pago", + "description": "Por favor espere mientras procesamos su pago" + } + }, + "dialog": { + "confirm": "Confirmar", + "cancel": "Cancelar", + "ok": "OK", + "iKnow": "Lo entiendo" + }, + "splash": { + "appName": "BearVPN", + "slogan": "Red global de alta velocidad", + "initializing": "Inicializando...", + "networkConnectionFailure": "Error de conexión de red, verifique e intente nuevamente", + "retry": "Reintentar", + "networkPermissionFailed": "Error al obtener permiso de red", + "initializationFailed": "Error de inicialización" + }, + "network": { + "status": { + "connected": "Conectado", + "disconnected": "Desconectado", + "connecting": "Conectando...", + "disconnecting": "Desconectando...", + "reconnecting": "Reconectando...", + "failed": "Error de conexión" + }, + "permission": { + "title": "Permiso de red", + "description": "Se requiere permiso de red para proporcionar el servicio VPN", + "goToSettings": "Ir a configuración", + "cancel": "Cancelar" + } + }, + "update": { + "title": "Actualización disponible", + "content": "¿Actualizar ahora?", + "updateNow": "Actualizar ahora", + "updateLater": "Más tarde", + "defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario" + }, + "kr_invite": { + "close": "Cerrar", + "saveQRCode": "Guardar código QR", + "qrCodeSaved": "Código QR guardado", + "shareLink": "Compartir enlace", + "shareQR": "Compartir código QR", + "myInviteCode": "Mi código de invitación" + }, + "country": { + "cn": "China", + "ir": "Irán", + "af": "Afganistán", + "ru": "Rusia", + "id": "Indonesia", + "tr": "Turquía", + "br": "Brasil" + }, + "error": { + "200": "Éxito", + "500": "Error interno del servidor", + "10001": "Error de consulta a la base de datos", + "10002": "Error de actualización de la base de datos", + "10003": "Error de inserción en la base de datos", + "10004": "Error de eliminación de la base de datos", + "20001": "El usuario ya existe", + "20002": "El usuario no existe", + "20003": "Contraseña de usuario incorrecta", + "20004": "Usuario deshabilitado", + "20005": "Saldo insuficiente", + "20006": "Registro detenido", + "20007": "Telegram no vinculado", + "20008": "Usuario no ha vinculado OAuth", + "20009": "Código de invitación incorrecto", + "30001": "El nodo ya existe", + "30002": "El nodo no existe", + "30003": "El grupo de nodos ya existe", + "30004": "El grupo de nodos no existe", + "30005": "El grupo de nodos no está vacío", + "400": "Error de parámetros", + "40002": "Token de usuario vacío", + "40003": "Token de usuario inválido", + "40004": "Token de usuario expirado", + "40005": "No ha iniciado sesión", + "401": "Demasiadas solicitudes", + "50001": "El cupón no existe", + "50002": "El cupón ya ha sido usado", + "50003": "El cupón no coincide", + "60001": "Suscripción expirada", + "60002": "Suscripción no disponible", + "60003": "El usuario ya tiene una suscripción", + "60004": "La suscripción ya ha sido usada", + "60005": "Límite de suscripción única excedido", + "60006": "Límite de cuota de suscripción", + "70001": "Código de verificación incorrecto", + "80001": "Error al encolar", + "90001": "Modo de depuración habilitado", + "90002": "Error al enviar SMS", + "90003": "Función SMS no habilitada", + "90004": "Función de correo electrónico no habilitada", + "90005": "Método de inicio de sesión no soportado", + "90006": "El autenticador no soporta este método", + "90007": "Código de país de teléfono vacío", + "90008": "Contraseña vacía", + "90009": "Código de país vacío", + "90010": "Se requiere contraseña o código de verificación", + "90011": "El correo electrónico ya existe", + "90012": "El número de teléfono ya existe", + "90013": "El dispositivo ya existe", + "90014": "Número de teléfono incorrecto", + "90015": "Este cuenta ha alcanzado el límite de envío hoy", + "90017": "El dispositivo no existe", + "90018": "ID de usuario no coincide", + "61001": "El pedido no existe", + "61002": "Método de pago no encontrado", + "61003": "Estado de pedido incorrecto", + "61004": "Período de reinicio insuficiente", + "61005": "Existe tráfico sin usar" + }, + "tray": { + "open_dashboard": "Abrir panel", + "copy_to_terminal": "Copiar al terminal", + "exit_app": "Salir de la aplicación" + } +} \ No newline at end of file diff --git a/assets/translations/strings_et.i18n.json b/assets/translations/strings_et.i18n.json index d4800bd..0ec3aa2 100755 --- a/assets/translations/strings_et.i18n.json +++ b/assets/translations/strings_et.i18n.json @@ -6,6 +6,7 @@ "codeSent": "6-kohaline kood on saadetud aadressile {account}. Palun sisesta see 30 minuti jooksul.", "back": "Tagasi", "enterEmailOrPhone": "Sisesta e-post või telefoninumber", + "enterEmail": "Please enter email address", "enterCode": "Palun sisesta kinnituskood", "enterPassword": "Palun sisesta parool", "reenterPassword": "Palun sisesta parool uuesti", diff --git a/assets/translations/strings_et.i18n.json.bak b/assets/translations/strings_et.i18n.json.bak new file mode 100755 index 0000000..d4800bd --- /dev/null +++ b/assets/translations/strings_et.i18n.json.bak @@ -0,0 +1,423 @@ +{ + "login": { + "welcome": "Tere tulemast BearVPN-i!", + "verifyPhone": "Kinnita oma telefoninumber", + "verifyEmail": "Kinnita oma e-post", + "codeSent": "6-kohaline kood on saadetud aadressile {account}. Palun sisesta see 30 minuti jooksul.", + "back": "Tagasi", + "enterEmailOrPhone": "Sisesta e-post või telefoninumber", + "enterCode": "Palun sisesta kinnituskood", + "enterPassword": "Palun sisesta parool", + "reenterPassword": "Palun sisesta parool uuesti", + "forgotPassword": "Unustasid parooli", + "codeLogin": "Koodiga sisselogimine", + "passwordLogin": "Parooliga sisselogimine", + "agreeTerms": "Logi sisse/Loo konto, nõustun", + "termsOfService": "Teenusetingimustega", + "privacyPolicy": "Privaatsuspoliitikaga", + "next": "Edasi", + "registerNow": "Registreeru kohe", + "setAndLogin": "Seadista ja logi sisse", + "enterAccount": "Palun sisesta konto", + "passwordMismatch": "Kaks sisestatud parooli ei ühti", + "sendCode": "Saada kood", + "codeSentCountdown": "Kood saadetud {seconds}s", + "and": "ja", + "enterInviteCode": "Sisesta kutse kood (valikuline)", + "registerSuccess": "Registreerimine õnnestus" + }, + "failure": { + "unexpected": "Ootamatu viga", + "clash": { + "unexpected": "Ootamatu viga", + "core": "Clash viga ${reason}" + }, + "singbox": { + "unexpected": "Ootamatu teenuse viga", + "serviceNotRunning": "Teenus ei tööta", + "missingPrivilege": "Puuduvad õigused", + "missingPrivilegeMsg": "VPN režiim vajab administraatori õigusi. Taaskäivitage rakendus administraatorina või muutke teenuse režiimi", + "missingGeoAssets": "Puuduvad GEO ressursid", + "missingGeoAssetsMsg": "Puuduvad GEO ressursifailid. Kaaluge aktiivsete ressursside muutmist või laadige valitud ressursid seadetest alla.", + "invalidConfigOptions": "Kehtetud seadistuse valikud", + "invalidConfig": "Kehtetu seadistus", + "create": "Teenuse loomise viga", + "start": "Teenuse käivitamise viga" + }, + "connectivity": { + "unexpected": "Ootamatu tõrge", + "missingVpnPermission": "Puudub VPN luba", + "missingNotificationPermission": "Puudub teavituste luba", + "core": "Tuuma viga" + }, + "profiles": { + "unexpected": "Ootamatu viga", + "notFound": "Profiili ei leitud", + "invalidConfig": "Kehtetu seadistus", + "invalidUrl": "Kehtetu URL" + }, + "connection": { + "unexpected": "Ootamatu ühenduse viga", + "timeout": "Ühenduse ajalõpp", + "badResponse": "Halb vastus", + "connectionError": "Ühenduse viga", + "badCertificate": "Kehtetu sertifikaat" + }, + "geoAssets": { + "unexpected": "Ootamatu viga", + "notUpdate": "Uuendusi pole saadaval", + "activeNotFound": "Aktiivseid GEO ressursse ei leitud" + } + }, + "userInfo": { + "title": "Minu info", + "bindingTip": "E-post/telefon sidumata", + "myAccount": "Minu konto", + "balance": "Jääk", + "noValidSubscription": "Teil pole kehtivat tellimust", + "subscribeNow": "Telli kohe", + "shortcuts": "Otseteed", + "adBlock": "Reklaami blokeerimine", + "dnsUnlock": "DNS avamine", + "contactUs": "Võta meiega ühendust", + "others": "Muu", + "logout": "Logi välja", + "logoutConfirmTitle": "Logi välja", + "logoutConfirmMessage": "Kas oled kindel, et soovid välja logida?", + "logoutCancel": "Tühista", + "vpnWebsite": "VPN veebileht", + "telegram": "Telegram", + "mail": "E-post", + "phone": "Telefon", + "customerService": "Klienditugi", + "workOrder": "Esita taotlus", + "pleaseLogin": "Palun logi esmalt sisse", + "subscriptionValid": "Tellimus kehtiv", + "startTime": "Algusaeg:", + "expireTime": "Aegumisaeg:", + "loginNow": "Logi kohe sisse", + "trialPeriod": "Tere tulemast Premium prooviperioodi", + "remainingTime": "Järelejäänud aeg", + "trialExpired": "Prooviperiood on lõppenud, ühendus katkestatud", + "subscriptionExpired": "Tellimus on aegunud, ühendus katkestatud", + "switchSubscription": "Vaheta tellimust", + "resetTrafficTitle": "Lähtesta liiklus", + "resetTrafficMessage": "Kuupaketi liikluse lähtestamise näide: lähtesta järgmise tsükli liiklus igakuiselt ja tellimuse kehtivusaeg edasi lükatakse {currentTime} kuni {newTime}", + "reset": "Lähtesta", + "deviceLimit": "Seadmete limiit: {count}", + "trafficUsage": "Kasutatud: {used} / {total}", + "trafficProgress": { + "title": "Liikluse kasutamine", + "unlimited": "Piiramatu liiklus", + "limited": "Kasutatud liiklus" + }, + "copySuccess": "Kopeeritud", + "notAvailable": "Pole saadaval", + "willBeDeleted": "kustutatakse", + "deleteAccountWarning": "Konto kustutamine on püsiv. Kui teie konto on kustutatud, ei saa te enam ühtegi funktsiooni kasutada. Jätkata?", + "requestDelete": "Taotle kustutamist" + }, + "setting": { + "title": "Seaded", + "vpnConnection": "VPN ühendus", + "general": "Üldine", + "autoConnect": "Automaatne ühendus", + "routeRule": "Marsruudi reeglid", + "countrySelector": "Vali riik", + "appearance": "Välimus", + "notifications": "Teavitused", + "helpImprove": "Aita meil paremaks muutuda", + "helpImproveSubtitle": "Aita meil paremaks muutuda alapealkiri", + "requestDeleteAccount": "Taotle konto kustutamist", + "goToDelete": "Mine kustutama", + "rateUs": "Hinda meid App Store'is", + "iosRating": "iOS hinnang", + "version": "Versioon", + "switchLanguage": "Vaheta keelt", + "system": "Süsteem", + "light": "Hele", + "dark": "Tume", + "vpnModeSmart": "Nutikas režiim", + "mode": "Väljundrežiim", + "connectionTypeGlobal": "Globaalne puhverserver", + "connectionTypeGlobalRemark": "Lubamisel suunatakse kogu liiklus puhverserveri kaudu", + "connectionTypeRule": "Nutikas puhverserver", + "connectionTypeRuleRemark": "Lubamisel, kui väljundrežiim on seatud nutikaks puhverserveriks, valitud riigi põhjal, jaotatakse liiklus automaatselt: kohalikud IP-d/domeenid ühenduvad otse, välismaised päringud suunatakse puhverserverisse", + "connectionTypeDirect": "Otsene ühendus", + "connectionTypeDirectRemark": "Lubamisel suunatakse kogu liiklus otse", + "smartMode": "Nutikas režiim", + "secureMode": "Turvaline režiim" + }, + "statistics": { + "title": "Statistika", + "vpnStatus": "VPN olek", + "ipAddress": "IP aadress", + "connectionTime": "Ühenduse aeg", + "protocol": "Protokoll", + "weeklyProtectionTime": "Iganädalane kaitseaeg", + "currentStreak": "Praegune seeria", + "highestStreak": "Parim seeria", + "longestConnection": "Pikim ühendus", + "days": "{days} päeva", + "daysOfWeek": { + "monday": "E", + "tuesday": "T", + "wednesday": "K", + "thursday": "N", + "friday": "R", + "saturday": "L", + "sunday": "P" + } + }, + "message": { + "title": "Teavitused", + "system": "Süsteemi sõnumid", + "promotion": "Kampaania sõnumid" + }, + "invite": { + "title": "Kutsu sõbrad", + "progress": "Kutse edenemine", + "inviteStats": "Kutse statistika", + "registers": "Registreeritud", + "totalCommission": "Kogu komisjonitasu", + "rewardDetails": "Tasu üksikasjad >", + "steps": "Kutse Sammud", + "inviteFriend": "Kutsu Sõbrad", + "acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru", + "getReward": "Saada Tasu", + "shareLink": "Jaga Linki", + "shareQR": "Jaga QR-koodi", + "rules": "Kutse Reeglid", + "rule1": "1. Saad kutsuda sõpru meiega liituma, jagades oma erilist kutselinki või kutsekoodi.", + "rule2": "2. Pärast seda, kui sõbrad on registreerunud ja sisse loginud, krediteeritakse kutsetasud automaatselt teie kontole.", + "pending": "Ootel", + "processing": "Töötlemisel", + "success": "Õnnestus", + "expired": "Aegunud", + "myInviteCode": "Minu Kutsekood", + "inviteCodeCopied": "Kutsekood kopeeritud lõikelauale", + "close": "Sulge", + "saveQRCode": "Salvesta QR-kood", + "qrCodeSaved": "QR-kood salvestatud", + "copiedToClipboard": "Kopeeritud lõikelauale", + "getInviteCodeFailed": "Kutsekoodi hankimine ebaõnnestus, proovi hiljem uuesti", + "generateQRCodeFailed": "QR-koodi genereerimine ebaõnnestus, proovi hiljem uuesti", + "generateShareLinkFailed": "Jagamislinki genereerimine ebaõnnestus, proovi hiljem uuesti" + }, + "purchaseMembership": { + "purchasePackage": "Paketi Ostmine", + "noData": "Saadaval pakette pole", + "myAccount": "Minu Konto", + "selectPackage": "Vali Pakett", + "packageDescription": "Paketi Kirjeldus", + "paymentMethod": "Makseviis", + "cancelAnytime": "Saad igal ajal rakenduses tühistada", + "startSubscription": "Alusta Tellimust", + "renewNow": "Uuenda Kohe", + "month": "{months} kuud", + "year": "{years} aastat", + "day": "{days} päeva", + "unlimitedTraffic": "Piiramatu Liiklus", + "unlimitedDevices": "Piiramatu Seadmete Arv", + "devices": "{count} seadet", + "features": "Paketi Funktsioonid", + "expand": "Laienda", + "collapse": "Sulge", + "confirmPurchase": "Kinnita Ost", + "confirmPurchaseDesc": "Kas olete kindel, et soovite selle paketi osta?" + }, + "orderStatus": { + "title": "Tellimuse olek", + "pending": { + "title": "Makse ootel", + "description": "Palun täitke makse" + }, + "paid": { + "title": "Makse vastu võetud", + "description": "Tellimust töödeldakse" + }, + "success": { + "title": "Palju õnne! Makse õnnestus", + "description": "Teie pakett on edukalt ostetud" + }, + "closed": { + "title": "Tellimus suletud", + "description": "Palun esitage uus tellimus" + }, + "failed": { + "title": "Makse ebaõnnestus", + "description": "Palun proovige makset uuesti" + }, + "unknown": { + "title": "Tundmatu olek", + "description": "Palun võtke ühendust klienditeenindusega" + }, + "checkFailed": { + "title": "Kontroll ebaõnnestus", + "description": "Palun proovige hiljem uuesti" + }, + "initial": { + "title": "Makset töödeldakse", + "description": "Palun oodake, kui töötleme teie makset" + } + }, + "home": { + "welcome": "Tere tulemast BearVPN-i", + "disconnected": "Ühendus katkestatud", + "connecting": "Ühendumine", + "connected": "Ühendatud", + "disconnecting": "Ühenduse katkestamine", + "currentConnectionTitle": "Praegune ühendus", + "switchNode": "Vaheta sõlme", + "timeout": "Aegumine", + "loading": "Laadimine...", + "error": "Laadimise viga", + "checkNetwork": "Kontrollige võrguühendust ja proovige uuesti", + "retry": "Proovi uuesti", + "connectionSectionTitle": "Ühendusviis", + "dedicatedServers": "Pühendatud serverid", + "countryRegion": "Riik/Regioon", + "serverListTitle": "Pühendatud serverite grupid", + "nodeListTitle": "Kõik sõlmed", + "countryListTitle": "Riikide/Regioonide nimekiri", + "noServers": "Saadaval pole ühtegi serverit", + "noNodes": "Saadaval pole ühtegi sõlme", + "noRegions": "Saadaval pole ühtegi regiooni", + "subscriptionDescription": "Hankige premium juurdepääs kiirele globaalsele võrgustikule", + "subscribe": "Telli", + "trialPeriod": "Tere tulemast Premium prooviversiooni", + "remainingTime": "Järelejäänud aeg", + "trialExpired": "Prooviaeg on lõppenud, ühendus katkestatud", + "subscriptionExpired": "Tellimus on aegunud, ühendus katkestatud", + "subscriptionUpdated": "Tellimus värskendatud", + "subscriptionUpdatedMessage": "Teie tellimuse teave on värskendatud, värskendage viimase oleku vaatamiseks", + "trialStatus": "Proovi olek", + "trialing": "Proovimas", + "trialEndMessage": "Prooviaja lõppedes ei saa enam kasutada", + "lastDaySubscriptionStatus": "Tellimus aegub varsti", + "lastDaySubscriptionMessage": "Aegub varsti", + "subscriptionEndMessage": "Tellimuse lõppedes ei saa enam kasutada", + "trialTimeWithDays": "{days}p {hours}t {minutes}m {seconds}s", + "trialTimeWithHours": "{hours}t {minutes}m {seconds}s", + "trialTimeWithMinutes": "{minutes}m {seconds}s", + "refreshLatency": "Värskenda latentsust", + "testLatency": "Testi latentsust", + "testing": "Latentsuse testimine", + "refreshLatencyDesc": "Värskenda kõigi sõlmede latentsust", + "testAllNodesLatency": "Testi kõigi sõlmede võrgu latentsust", + "autoSelect": "Automaatne valik", + "selected": "Valitud" + }, + "dialog": { + "confirm": "Kinnita", + "cancel": "Tühista", + "ok": "OK" + }, + "splash": { + "appName": "BearVPN", + "slogan": "Kiire globaalne võrgustik", + "initializing": "Initsialiseerimine...", + "networkConnectionFailure": "Võrguühenduse viga, kontrollige ja proovige uuesti", + "retry": "Proovi uuesti", + "networkPermissionFailed": "Võrguõiguse hankimine ebaõnnestus", + "initializationFailed": "Initsialiseerimine ebaõnnestus" + }, + "network": { + "status": { + "connected": "Ühendatud", + "disconnected": "Ühendus katkestatud", + "connecting": "Ühendumine...", + "disconnecting": "Ühenduse katkestamine...", + "reconnecting": "Ühenduse taastamine...", + "failed": "Ühenduse viga" + }, + "permission": { + "title": "Võrguõigus", + "description": "VPN teenuse pakkumiseks on vaja võrguõigust", + "goToSettings": "Mine seadetesse", + "cancel": "Tühista" + } + }, + "update": { + "title": "Uuendus saadaval", + "content": "Uuendada nüüd?", + "updateNow": "Uuenda nüüd", + "updateLater": "Hiljem", + "defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine" + }, + "country": { + "cn": "Hiina", + "ir": "Iraan", + "af": "Afganistan", + "ru": "Venemaa", + "id": "Indoneesia", + "tr": "Türgi", + "br": "Brasiilia" + }, + "error": { + "200": "Edukas", + "500": "Serveri sisemine viga", + "10001": "Andmebaasi päringu viga", + "10002": "Andmebaasi uuendamise viga", + "10003": "Andmebaasi sisestamise viga", + "10004": "Andmebaasi kustutamise viga", + "20001": "Kasutaja on juba olemas", + "20002": "Kasutajat pole olemas", + "20003": "Vale kasutaja parool", + "20004": "Kasutaja on keelatud", + "20005": "Ebapiisav saldo", + "20006": "Registreerimine peatatud", + "20007": "Telegram pole seotud", + "20008": "Kasutaja pole OAuth-i seostanud", + "20009": "Vale kutsekood", + "30001": "Sõlm on juba olemas", + "30002": "Sõlme pole olemas", + "30003": "Sõlme grupp on juba olemas", + "30004": "Sõlme gruppi pole olemas", + "30005": "Sõlme grupp pole tühi", + "400": "Parameetri viga", + "40002": "Kasutaja token on tühi", + "40003": "Vale kasutaja token", + "40004": "Kasutaja token on aegunud", + "40005": "Te pole sisse logitud", + "401": "Liiga palju päringuid", + "50001": "Kupongi pole olemas", + "50002": "Kupong on juba kasutatud", + "50003": "Kupong ei vasta", + "60001": "Tellimus on aegunud", + "60002": "Tellimus pole saadaval", + "60003": "Kasutajal on juba tellimus", + "60004": "Tellimus on juba kasutatud", + "60005": "Üksiku tellimuse režiimi limiit ületatud", + "60006": "Tellimuse kvoodi limiit", + "70001": "Vale kinnituskood", + "80001": "Järjekorda lisamise viga", + "90001": "Silumisrežiim on lubatud", + "90002": "SMS-i saatmise viga", + "90003": "SMS funktsioon pole lubatud", + "90004": "E-posti funktsioon pole lubatud", + "90005": "Toetamata sisselogimise meetod", + "90006": "Autentifikaator ei toeta seda meetodit", + "90007": "Telefoni riigi kood on tühi", + "90008": "Parool on tühi", + "90009": "Riigi kood on tühi", + "90010": "Parool või kinnituskood on vajalik", + "90011": "E-post on juba olemas", + "90012": "Telefoninumber on juba olemas", + "90013": "Seade on juba olemas", + "90014": "Vale telefoninumber", + "90015": "See konto on täna saavutanud saatmise limiidi", + "90017": "Seadet pole olemas", + "90018": "Kasutaja ID ei vasta", + "61001": "Tellimust pole olemas", + "61002": "Makseviisi ei leitud", + "61003": "Vale tellimuse olek", + "61004": "Ebapiisav lähtestamise periood", + "61005": "Kasutamata liiklus on olemas" + }, + "tray": { + "open_dashboard": "Ava armatuurlaud", + "copy_to_terminal": "Kopeeri terminali", + "exit_app": "Välju rakendusest" + } +} \ No newline at end of file diff --git a/assets/translations/strings_ja.i18n.json b/assets/translations/strings_ja.i18n.json index 565e258..e5879e6 100755 --- a/assets/translations/strings_ja.i18n.json +++ b/assets/translations/strings_ja.i18n.json @@ -6,6 +6,7 @@ "codeSent": "{account}に6桁のコードを送信しました。30分以内に入力してください。", "back": "戻る", "enterEmailOrPhone": "メールアドレスまたは電話番号を入力", + "enterEmail": "Please enter email address", "enterCode": "認証コードを入力してください", "enterPassword": "パスワードを入力してください", "reenterPassword": "パスワードを再入力してください", diff --git a/assets/translations/strings_ja.i18n.json.bak b/assets/translations/strings_ja.i18n.json.bak new file mode 100755 index 0000000..565e258 --- /dev/null +++ b/assets/translations/strings_ja.i18n.json.bak @@ -0,0 +1,441 @@ +{ + "login": { + "welcome": "BearVPNへようこそ!", + "verifyPhone": "電話番号を認証", + "verifyEmail": "メールアドレスを認証", + "codeSent": "{account}に6桁のコードを送信しました。30分以内に入力してください。", + "back": "戻る", + "enterEmailOrPhone": "メールアドレスまたは電話番号を入力", + "enterCode": "認証コードを入力してください", + "enterPassword": "パスワードを入力してください", + "reenterPassword": "パスワードを再入力してください", + "forgotPassword": "パスワードをお忘れの方", + "codeLogin": "認証コードでログイン", + "passwordLogin": "パスワードでログイン", + "agreeTerms": "ログイン/アカウント作成,に同意します", + "termsOfService": "利用規約", + "privacyPolicy": "プライバシーポリシー", + "next": "次へ", + "registerNow": "今すぐ登録", + "setAndLogin": "設定してログイン", + "enterAccount": "アカウントを入力してください", + "passwordMismatch": "2回のパスワード入力が一致しません", + "sendCode": "認証コードを送信", + "codeSentCountdown": "認証コード送信済み {seconds}秒", + "and": "および", + "enterInviteCode": "招待コードを入力(任意)", + "registerSuccess": "登録成功" + }, + "failure": { + "unexpected": "予期せぬエラー", + "clash": { + "unexpected": "予期せぬエラー", + "core": "Clashエラー ${reason}" + }, + "singbox": { + "unexpected": "予期せぬサービスエラー", + "serviceNotRunning": "サービスが実行されていません", + "missingPrivilege": "権限がありません", + "missingPrivilegeMsg": "VPNモードには管理者権限が必要です。管理者として再起動するか、サービスモードを変更してください", + "missingGeoAssets": "GEOリソースファイルがありません", + "missingGeoAssetsMsg": "GEOリソースファイルがありません。アクティブなリソースを変更するか、設定で選択したリソースをダウンロードしてください。", + "invalidConfigOptions": "設定オプションが無効です", + "invalidConfig": "無効な設定", + "create": "サービス作成エラー", + "start": "サービス起動エラー" + }, + "connectivity": { + "unexpected": "予期せぬ失敗", + "missingVpnPermission": "VPN権限がありません", + "missingNotificationPermission": "通知権限がありません", + "core": "コアエラー" + }, + "profiles": { + "unexpected": "予期せぬエラー", + "notFound": "プロファイルが見つかりません", + "invalidConfig": "無効な設定", + "invalidUrl": "無効なURL" + }, + "connection": { + "unexpected": "予期せぬ接続エラー", + "timeout": "接続タイムアウト", + "badResponse": "不正なレスポンス", + "connectionError": "接続エラー", + "badCertificate": "無効な証明書" + }, + "geoAssets": { + "unexpected": "予期せぬエラー", + "notUpdate": "利用可能な更新はありません", + "activeNotFound": "アクティブなGEOリソースファイルが見つかりません" + } + }, + "userInfo": { + "title": "マイ情報", + "bindingTip": "メール/電話番号が未登録です", + "myAccount": "マイアカウント", + "balance": "残高", + "noValidSubscription": "有効なサブスクリプションがありません", + "subscribeNow": "今すぐ購読", + "shortcuts": "ショートカット", + "adBlock": "広告ブロック", + "dnsUnlock": "DNSアンロック", + "contactUs": "お問い合わせ", + "others": "その他", + "logout": "ログアウト", + "logoutConfirmTitle": "ログアウト", + "logoutConfirmMessage": "ログアウトしますか?", + "logoutCancel": "キャンセル", + "vpnWebsite": "VPN公式サイト", + "telegram": "Telegram", + "mail": "メール", + "phone": "電話", + "customerService": "カスタマーサービス", + "workOrder": "チケット送信", + "pleaseLogin": "ログインしてください", + "subscriptionValid": "サブスクリプション有効", + "startTime": "開始時間:", + "expireTime": "有効期限:", + "loginNow": "今すぐログイン", + "trialPeriod": "トライアル期間", + "remainingTime": "残り時間", + "trialExpired": "トライアル期間が終了しました", + "subscriptionExpired": "サブスクリプションが期限切れです", + "copySuccess": "コピーしました", + "notAvailable": "利用不可", + "willBeDeleted": "削除されます", + "deleteAccountWarning": "アカウントの削除は永久的です。アカウントを削除すると、すべての機能が使用できなくなります。続行しますか?", + "requestDelete": "削除をリクエスト", + "deviceLimit": "デバイス制限:{count}", + "reset": "リセット", + "trafficUsage": "トラフィック:{used} / {total}", + "trafficProgress": { + "title": "トラフィック使用状況", + "unlimited": "無制限", + "limited": "使用済み" + }, + "switchSubscription": "サブスクリプションの切り替え", + "resetTrafficTitle": "トラフィックリセット", + "resetTrafficMessage": "月間プランのトラフィックリセット例:次のサイクルのトラフィックを月次でリセットし、サブスクリプションの有効期限が{currentTime}から{newTime}に繰り上げられます", + "trialStatus": "トライアル状態", + "trialing": "トライアル中", + "trialEndMessage": "トライアル期間終了後は使用できなくなります", + "lastDaySubscriptionStatus": "サブスクリプション期限切れ間近", + "lastDaySubscriptionMessage": "期限切れ間近", + "subscriptionEndMessage": "サブスクリプション終了後は使用できなくなります", + "trialTimeWithDays": "{days}日{hours}時間{minutes}分{seconds}秒", + "trialTimeWithHours": "{hours}時間{minutes}分{seconds}秒", + "trialTimeWithMinutes": "{minutes}分{seconds}秒", + "refreshLatency": "レイテンシーを更新", + "testLatency": "レイテンシーテスト", + "testing": "レイテンシーテスト中", + "refreshLatencyDesc": "すべてのノードのレイテンシーを更新", + "testAllNodesLatency": "すべてのノードのネットワークレイテンシーをテスト", + "autoSelect": "自動選択", + "selected": "選択済み" + }, + "setting": { + "title": "設定", + "vpnConnection": "VPN接続", + "countrySelector": "国を選択", + "general": "一般", + "autoConnect": "自動接続", + "routeRule": "ルーティングルール", + "appearance": "外観", + "notifications": "通知", + "helpImprove": "改善にご協力ください", + "helpImproveSubtitle": "改善にご協力くださいサブタイトル", + "requestDeleteAccount": "アカウント削除をリクエスト", + "goToDelete": "削除へ", + "rateUs": "App Storeで評価する", + "iosRating": "iOS評価", + "version": "バージョン", + "switchLanguage": "言語を切り替え", + "system": "システム", + "light": "ライト", + "dark": "ダーク", + "vpnModeSmart": "スマートモード", + "mode": "アウトバウンドモード", + "connectionTypeGlobal": "グローバルプロキシ", + "connectionTypeGlobalRemark": "有効時、すべてのトラフィックはプロキシ経由でルーティングされます", + "connectionTypeRule": "スマートプロキシ", + "connectionTypeRuleRemark": "[アウトバウンドモード]が[スマートプロキシ]に設定されている場合、システムは選択された国に基づいて国内と海外のトラフィックを自動的に分割します:国内IP/ドメインは直接接続、海外リクエストはプロキシ経由でアクセス", + "connectionTypeDirect": "ダイレクト接続", + "connectionTypeDirectRemark": "有効時、すべてのトラフィックはプロキシをバイパスします", + "smartMode": "スマートモード", + "secureMode": "セキュアモード" + }, + "statistics": { + "title": "統計", + "vpnStatus": "VPNステータス", + "ipAddress": "IPアドレス", + "connectionTime": "接続時間", + "protocol": "プロトコル", + "weeklyProtectionTime": "週間保護時間", + "currentStreak": "現在の連続記録", + "highestStreak": "最高記録", + "longestConnection": "最長接続時間", + "days": "{days}日", + "daysOfWeek": { + "monday": "月", + "tuesday": "火", + "wednesday": "水", + "thursday": "木", + "friday": "金", + "saturday": "土", + "sunday": "日" + } + }, + "message": { + "title": "通知", + "system": "システムメッセージ", + "promotion": "プロモーションメッセージ" + }, + "invite": { + "title": "友達を招待", + "progress": "招待の進捗", + "inviteStats": "招待統計", + "registers": "登録済み", + "totalCommission": "総報酬", + "rewardDetails": "報酬の詳細 >", + "steps": "招待の手順", + "inviteFriend": "友達を招待", + "acceptInvite": "友達が招待を受け入れ\n注文して登録", + "getReward": "報酬を獲得", + "shareLink": "リンクを共有", + "shareQR": "QRコードを共有", + "rules": "招待ルール", + "rule1": "1. 専用の招待リンクまたは招待コードを共有して、友達を招待できます。", + "rule2": "2. 友達が登録とログインを完了すると、招待報酬が自動的にアカウントに付与されます。", + "pending": "保留中", + "processing": "処理中", + "success": "成功", + "expired": "期限切れ", + "myInviteCode": "招待コード", + "inviteCodeCopied": "招待コードをクリップボードにコピーしました", + "close": "閉じる", + "saveQRCode": "QRコードを保存", + "qrCodeSaved": "QRコードを保存しました", + "copiedToClipboard": "クリップボードにコピーしました", + "getInviteCodeFailed": "招待コードの取得に失敗しました。後ほど再試行してください", + "generateQRCodeFailed": "QRコードの生成に失敗しました。後ほど再試行してください", + "generateShareLinkFailed": "共有リンクの生成に失敗しました。後ほど再試行してください" + }, + "purchaseMembership": { + "purchasePackage": "パッケージ購入", + "noData": "利用可能なパッケージはありません", + "myAccount": "マイアカウント", + "selectPackage": "パッケージを選択", + "packageDescription": "パッケージ説明", + "paymentMethod": "支払い方法", + "cancelAnytime": "アプリでいつでもキャンセル可能", + "startSubscription": "サブスクリプションを開始", + "renewNow": "今すぐ更新", + "month": "{months}ヶ月", + "year": "{years}年", + "day": "{days}日", + "unlimitedTraffic": "無制限トラフィック", + "unlimitedDevices": "無制限デバイス", + "devices": "{count}台", + "features": "パッケージ機能", + "expand": "展開", + "collapse": "折りたたむ", + "confirmPurchase": "購入を確認", + "confirmPurchaseDesc": "このパッケージを購入してもよろしいですか?", + "subscriptionPrivacyInfo": "サブスクリプションとプライバシー情報" + }, + "orderStatus": { + "title": "注文状態", + "pending": { + "title": "支払い待ち", + "description": "支払いを完了してください" + }, + "paid": { + "title": "支払い完了", + "description": "注文を処理中です" + }, + "success": { + "title": "おめでとうございます!支払い成功", + "description": "パッケージの購入が完了しました" + }, + "closed": { + "title": "注文キャンセル", + "description": "新規注文をお願いします" + }, + "failed": { + "title": "支払い失敗", + "description": "支払いを再試行してください" + }, + "unknown": { + "title": "不明な状態", + "description": "カスタマーサービスにお問い合わせください" + }, + "checkFailed": { + "title": "確認失敗", + "description": "後でもう一度お試しください" + }, + "initial": { + "title": "支払い処理中", + "description": "支払い処理中です。お待ちください" + } + }, + "home": { + "welcome": "BearVPNへようこそ", + "disconnected": "未接続", + "connecting": "接続中", + "connected": "接続済み", + "disconnecting": "切断中", + "currentConnectionTitle": "現在の接続", + "switchNode": "ノード切替", + "timeout": "タイムアウト", + "loading": "読み込み中...", + "error": "読み込みエラー", + "checkNetwork": "ネットワーク接続を確認して再試行してください", + "retry": "再試行", + "connectionSectionTitle": "接続方法", + "dedicatedServers": "専用サーバー", + "countryRegion": "国/地域", + "serverListTitle": "専用サーバーグループ", + "nodeListTitle": "全ノード", + "countryListTitle": "国/地域リスト", + "noServers": "利用可能なサーバーがありません", + "noNodes": "利用可能なノードがありません", + "noRegions": "利用可能な地域がありません", + "subscriptionDescription": "プレミアムアクセスで高速グローバルネットワークを利用", + "subscribe": "購読する", + "trialPeriod": "プレミアムトライアルへようこそ", + "remainingTime": "残り時間", + "trialExpired": "トライアル期間が終了し、接続が切断されました", + "subscriptionExpired": "サブスクリプションが期限切れとなり、接続が切断されました", + "subscriptionUpdated": "サブスクリプションが更新されました", + "subscriptionUpdatedMessage": "サブスクリプション情報が更新されました。最新の状態を確認するには更新してください", + "trialStatus": "トライアル状態", + "trialing": "トライアル中", + "trialEndMessage": "トライアル期間終了後は使用できなくなります", + "lastDaySubscriptionStatus": "サブスクリプション期限切れ間近", + "lastDaySubscriptionMessage": "期限切れ間近", + "subscriptionEndMessage": "サブスクリプション終了後は使用できなくなります", + "trialTimeWithDays": "{days}日{hours}時間{minutes}分{seconds}秒", + "trialTimeWithHours": "{hours}時間{minutes}分{seconds}秒", + "trialTimeWithMinutes": "{minutes}分{seconds}秒", + "refreshLatency": "レイテンシー更新", + "testLatency": "レイテンシーテスト", + "testing": "レイテンシーテスト中", + "refreshLatencyDesc": "全ノードのレイテンシーを更新", + "testAllNodesLatency": "全ノードのネットワークレイテンシーをテスト", + "autoSelect": "自動選択", + "selected": "選択済み" + }, + "dialog": { + "confirm": "確認", + "cancel": "キャンセル", + "ok": "OK", + "iKnow": "分かりました" + }, + "splash": { + "appName": "BearVPN", + "slogan": "高速グローバルネットワーク", + "initializing": "初期化中...", + "networkConnectionFailure": "ネットワーク接続エラー、確認して再試行してください", + "retry": "再試行", + "networkPermissionFailed": "ネットワーク権限の取得に失敗しました", + "initializationFailed": "初期化に失敗しました" + }, + "network": { + "status": { + "connected": "接続済み", + "disconnected": "未接続", + "connecting": "接続中...", + "disconnecting": "切断中...", + "reconnecting": "再接続中...", + "failed": "接続エラー" + }, + "permission": { + "title": "ネットワーク権限", + "description": "VPNサービスを提供するにはネットワーク権限が必要です", + "goToSettings": "設定へ", + "cancel": "キャンセル" + } + }, + "update": { + "title": "アップデートが利用可能", + "content": "今すぐアップデートしますか?", + "updateNow": "今すぐアップデート", + "updateLater": "後で", + "defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上" + }, + "country": { + "cn": "中国", + "ir": "イラン", + "af": "アフガニスタン", + "ru": "ロシア", + "id": "インドネシア", + "tr": "トルコ", + "br": "ブラジル" + }, + "error": { + "200": "成功", + "500": "サーバー内部エラー", + "10001": "データベースクエリエラー", + "10002": "データベース更新エラー", + "10003": "データベース挿入エラー", + "10004": "データベース削除エラー", + "20001": "ユーザーは既に存在します", + "20002": "ユーザーが存在しません", + "20003": "ユーザーパスワードが間違っています", + "20004": "ユーザーは無効化されています", + "20005": "残高不足", + "20006": "登録は停止されています", + "20007": "Telegramが未連携です", + "20008": "ユーザーがOAuthを連携していません", + "20009": "招待コードが間違っています", + "30001": "ノードは既に存在します", + "30002": "ノードが存在しません", + "30003": "ノードグループは既に存在します", + "30004": "ノードグループが存在しません", + "30005": "ノードグループは空ではありません", + "400": "パラメータエラー", + "40002": "ユーザートークンが空です", + "40003": "ユーザートークンが無効です", + "40004": "ユーザートークンの有効期限が切れています", + "40005": "ログインしていません", + "401": "リクエストが多すぎます", + "50001": "クーポンが存在しません", + "50002": "クーポンは既に使用されています", + "50003": "クーポンが一致しません", + "60001": "サブスクリプションの有効期限が切れています", + "60002": "サブスクリプションは利用できません", + "60003": "ユーザーは既にサブスクリプションを持っています", + "60004": "サブスクリプションは既に使用されています", + "60005": "単一サブスクリプションモードの制限を超えています", + "60006": "サブスクリプションクォータ制限", + "70001": "認証コードが間違っています", + "80001": "キューイングエラー", + "90001": "デバッグモードが有効です", + "90002": "SMS送信エラー", + "90003": "SMS機能が有効になっていません", + "90004": "メール機能が有効になっていません", + "90005": "サポートされていないログイン方法", + "90006": "認証器がこの方法をサポートしていません", + "90007": "電話国番号が空です", + "90008": "パスワードが空です", + "90009": "国番号が空です", + "90010": "パスワードまたは認証コードが必要です", + "90011": "メールアドレスは既に存在します", + "90012": "電話番号は既に存在します", + "90013": "デバイスは既に存在します", + "90014": "電話番号が間違っています", + "90015": "このアカウントは本日の送信制限に達しました", + "90017": "デバイスが存在しません", + "90018": "ユーザーIDが一致しません", + "61001": "注文が存在しません", + "61002": "支払い方法が見つかりません", + "61003": "注文状態が間違っています", + "61004": "リセット期間が不足しています", + "61005": "未使用のトラフィックが存在します" + }, + "tray": { + "open_dashboard": "ダッシュボードを開く", + "copy_to_terminal": "ターミナルにコピー", + "exit_app": "アプリを終了" + } +} \ No newline at end of file diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index ceb853e..eabfc49 100755 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -6,6 +6,7 @@ "codeSent": "6-значный код отправлен на {account}. Введите его в течение 30 минут.", "back": "Назад", "enterEmailOrPhone": "Введите email или номер телефона", + "enterEmail": "Please enter email address", "enterCode": "Введите код подтверждения", "enterPassword": "Введите пароль", "reenterPassword": "Повторите пароль", diff --git a/assets/translations/strings_ru.i18n.json.bak b/assets/translations/strings_ru.i18n.json.bak new file mode 100755 index 0000000..ceb853e --- /dev/null +++ b/assets/translations/strings_ru.i18n.json.bak @@ -0,0 +1,442 @@ +{ + "login": { + "welcome": "Добро пожаловать в BearVPN!", + "verifyPhone": "Подтвердите номер телефона", + "verifyEmail": "Подтвердите email", + "codeSent": "6-значный код отправлен на {account}. Введите его в течение 30 минут.", + "back": "Назад", + "enterEmailOrPhone": "Введите email или номер телефона", + "enterCode": "Введите код подтверждения", + "enterPassword": "Введите пароль", + "reenterPassword": "Повторите пароль", + "forgotPassword": "Забыли пароль", + "codeLogin": "Вход по коду", + "passwordLogin": "Вход по паролю", + "agreeTerms": "Вход/Создать аккаунт, я соглашаюсь с", + "termsOfService": "Условиями использования", + "privacyPolicy": "Политикой конфиденциальности", + "next": "Далее", + "registerNow": "Зарегистрироваться", + "setAndLogin": "Установить и войти", + "enterAccount": "Введите аккаунт", + "passwordMismatch": "Два введенных пароля не совпадают", + "sendCode": "Отправить код", + "codeSentCountdown": "Код отправлен {seconds}с", + "and": "и", + "enterInviteCode": "Введите код приглашения (необязательно)", + "registerSuccess": "Регистрация успешна" + }, + "failure": { + "unexpected": "Непредвиденная ошибка", + "clash": { + "unexpected": "Непредвиденная ошибка", + "core": "Ошибка Clash ${reason}" + }, + "singbox": { + "unexpected": "Непредвиденная ошибка сервиса", + "serviceNotRunning": "Сервис не запущен", + "missingPrivilege": "Отсутствуют привилегии", + "missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение с правами администратора или измените режим сервиса", + "missingGeoAssets": "Отсутствуют GEO-ресурсы", + "missingGeoAssetsMsg": "Отсутствуют файлы GEO-ресурсов. Измените активные ресурсы или загрузите выбранные ресурсы в настройках.", + "invalidConfigOptions": "Недопустимые параметры конфигурации", + "invalidConfig": "Недопустимая конфигурация", + "create": "Ошибка создания сервиса", + "start": "Ошибка запуска сервиса" + }, + "connectivity": { + "unexpected": "Непредвиденный сбой", + "missingVpnPermission": "Отсутствует разрешение VPN", + "missingNotificationPermission": "Отсутствует разрешение на уведомления", + "core": "Ошибка ядра" + }, + "profiles": { + "unexpected": "Непредвиденная ошибка", + "notFound": "Профиль не найден", + "invalidConfig": "Недопустимая конфигурация", + "invalidUrl": "Недопустимый URL" + }, + "connection": { + "unexpected": "Непредвиденная ошибка подключения", + "timeout": "Тайм-аут подключения", + "badResponse": "Некорректный ответ", + "connectionError": "Ошибка подключения", + "badCertificate": "Недействительный сертификат" + }, + "geoAssets": { + "unexpected": "Непредвиденная ошибка", + "notUpdate": "Нет доступных обновлений", + "activeNotFound": "Активные GEO-ресурсы не найдены" + } + }, + "userInfo": { + "title": "Моя информация", + "bindingTip": "Email/телефон не привязаны", + "myAccount": "Мой аккаунт", + "balance": "Баланс", + "noValidSubscription": "Нет активной подписки", + "subscribeNow": "Подписаться сейчас", + "shortcuts": "Ярлыки", + "adBlock": "Блокировка рекламы", + "dnsUnlock": "Разблокировка DNS", + "contactUs": "Связаться с нами", + "others": "Прочее", + "logout": "Выйти", + "logoutConfirmTitle": "Выйти", + "logoutConfirmMessage": "Вы уверены, что хотите выйти?", + "logoutCancel": "Отмена", + "vpnWebsite": "Сайт VPN", + "telegram": "Telegram", + "mail": "Email", + "phone": "Телефон", + "customerService": "Служба поддержки", + "workOrder": "Создать заявку", + "pleaseLogin": "Пожалуйста, войдите", + "subscriptionValid": "Подписка активна", + "startTime": "Время начала:", + "expireTime": "Срок действия:", + "loginNow": "Войти сейчас", + "trialPeriod": "Добро пожаловать в пробную версию Premium", + "remainingTime": "Осталось времени", + "trialExpired": "Пробный период истёк, соединение разорвано", + "subscriptionExpired": "Подписка истекла, соединение разорвано", + "switchSubscription": "Сменить подписку", + "resetTrafficTitle": "Сброс трафика", + "resetTrafficMessage": "Пример сброса трафика месячного плана: сброс трафика следующего цикла ежемесячно, и срок действия подписки будет перенесен с {currentTime} на {newTime}", + "reset": "Сбросить", + "trafficUsage": "Использовано: {used} / {total}", + "trafficProgress": { + "title": "Использование трафика", + "unlimited": "Безлимитный трафик", + "limited": "Использованный трафик" + }, + "deviceLimit": "Лимит устройств: {count}", + "copySuccess": "Скопировано успешно", + "notAvailable": "Недоступно", + "willBeDeleted": "Будет удалено", + "deleteAccountWarning": "Удаление аккаунта необратимо. После удаления аккаунта вы не сможете использовать все функции. Продолжить?", + "requestDelete": "Запросить удаление", + "trialStatus": "Статус пробного периода", + "trialing": "Пробный период", + "trialEndMessage": "После окончания пробного периода использование будет невозможно", + "lastDaySubscriptionStatus": "Подписка скоро истечет", + "lastDaySubscriptionMessage": "Скоро истечет", + "subscriptionEndMessage": "После окончания подписки использование будет невозможно", + "trialTimeWithDays": "{days}д {hours}ч {minutes}м {seconds}с", + "trialTimeWithHours": "{hours}ч {minutes}м {seconds}с", + "trialTimeWithMinutes": "{minutes}м {seconds}с", + "refreshLatency": "Обновить задержку", + "testLatency": "Тест задержки", + "testing": "Тестирование задержки", + "refreshLatencyDesc": "Обновить задержку всех узлов", + "testAllNodesLatency": "Тест сетевой задержки всех узлов", + "autoSelect": "Автоматический выбор", + "selected": "Выбрано" + }, + "setting": { + "title": "Настройки", + "countrySelector": "Выбрать страну", + "vpnConnection": "VPN-подключение", + "general": "Общие", + "autoConnect": "Автоподключение", + "routeRule": "Правила маршрутизации", + "appearance": "Внешний вид", + "notifications": "Уведомления", + "helpImprove": "Помогите нам улучшить", + "helpImproveSubtitle": "Подзаголовок помощи в улучшении", + "requestDeleteAccount": "Запросить удаление аккаунта", + "goToDelete": "Перейти к удалению", + "rateUs": "Оцените нас в App Store", + "iosRating": "Оценка iOS", + "version": "Версия", + "switchLanguage": "Сменить язык", + "system": "Система", + "light": "Светлая", + "dark": "Тёмная", + "vpnModeSmart": "Умный режим", + "mode": "Исходящий режим", + "connectionTypeGlobal": "Глобальный прокси", + "connectionTypeGlobalRemark": "При включении весь трафик проходит через прокси", + "connectionTypeRule": "Умный прокси", + "connectionTypeRuleRemark": "Когда [Исходящий режим] установлен на [Умный прокси], система автоматически разделяет внутренний и международный трафик в соответствии с выбранной страной: внутренние IP/домены подключаются напрямую, а зарубежные запросы проходят через прокси", + "connectionTypeDirect": "Прямое подключение", + "connectionTypeDirectRemark": "При включении весь трафик обходит прокси", + "smartMode": "Умный режим", + "secureMode": "Безопасный режим" + }, + "statistics": { + "title": "Статистика", + "vpnStatus": "Статус VPN", + "ipAddress": "IP-адрес", + "connectionTime": "Время подключения", + "protocol": "Протокол", + "weeklyProtectionTime": "Время защиты за неделю", + "currentStreak": "Текущая серия", + "highestStreak": "Рекорд", + "longestConnection": "Самое долгое подключение", + "days": "{days} дн.", + "daysOfWeek": { + "monday": "Пн", + "tuesday": "Вт", + "wednesday": "Ср", + "thursday": "Чт", + "friday": "Пт", + "saturday": "Сб", + "sunday": "Вс" + } + }, + "message": { + "title": "Уведомления", + "system": "Системные сообщения", + "promotion": "Рекламные сообщения" + }, + "invite": { + "title": "Пригласить друзей", + "progress": "Прогресс приглашений", + "inviteStats": "Статистика приглашений", + "registers": "Зарегистрировано", + "totalCommission": "Общая комиссия", + "rewardDetails": "Детали вознаграждения >", + "steps": "Шаги Приглашения", + "inviteFriend": "Пригласить Друзей", + "acceptInvite": "Друзья принимают приглашение", + "getReward": "Получить Вознаграждение", + "shareLink": "Поделиться Ссылкой", + "shareQR": "Поделиться QR-кодом", + "rules": "Правила Приглашения", + "rule1": "1. Вы можете приглашать друзей присоединиться к нам, поделившись своей уникальной ссылкой или кодом приглашения.", + "rule2": "2. После того, как друзья завершат регистрацию и войдут в систему, вознаграждения за приглашение будут автоматически зачислены на ваш счет.", + "pending": "В Ожидании", + "processing": "В Обработке", + "success": "Успешно", + "expired": "Истекло", + "myInviteCode": "Мой Код Приглашения", + "inviteCodeCopied": "Код приглашения скопирован в буфер обмена", + "close": "Закрыть", + "saveQRCode": "Сохранить QR-код", + "qrCodeSaved": "QR-код сохранен", + "copiedToClipboard": "Скопировано в буфер обмена", + "getInviteCodeFailed": "Не удалось получить код приглашения, попробуйте позже", + "generateQRCodeFailed": "Не удалось сгенерировать QR-код, попробуйте позже", + "generateShareLinkFailed": "Не удалось сгенерировать ссылку для общего доступа, попробуйте позже" + }, + "purchaseMembership": { + "purchasePackage": "Купить Пакет", + "noData": "Нет доступных пакетов", + "myAccount": "Мой Аккаунт", + "selectPackage": "Выбрать Пакет", + "packageDescription": "Описание Пакета", + "paymentMethod": "Способ Оплаты", + "cancelAnytime": "Вы можете отменить в любое время в приложении", + "startSubscription": "Начать Подписку", + "subscriptionPrivacyInfo": "Информация о Подписке и Конфиденциальности", + "month": "{months} Месяц(ев)", + "year": "{years} Год(а)", + "day": "{days} день", + "unlimitedTraffic": "Безлимитный трафик", + "unlimitedDevices": "Безлимитные устройства", + "devices": "{count} устройств", + "trafficLimit": "Ограничение трафика", + "deviceLimit": "Лимит устройств: {count}", + "features": "Функции пакета", + "expand": "Развернуть", + "collapse": "Свернуть", + "confirmPurchase": "Подтвердить покупку", + "confirmPurchaseDesc": "Вы уверены, что хотите приобрести этот пакет?" + }, + "orderStatus": { + "title": "Статус заказа", + "pending": { + "title": "Ожидание оплаты", + "description": "Пожалуйста, завершите оплату" + }, + "paid": { + "title": "Оплата получена", + "description": "Обработка вашего заказа" + }, + "success": { + "title": "Поздравляем! Оплата успешна", + "description": "Ваш пакет успешно приобретен" + }, + "closed": { + "title": "Заказ закрыт", + "description": "Пожалуйста, сделайте новый заказ" + }, + "failed": { + "title": "Ошибка оплаты", + "description": "Пожалуйста, попробуйте оплату снова" + }, + "unknown": { + "title": "Неизвестный статус", + "description": "Пожалуйста, свяжитесь со службой поддержки" + }, + "checkFailed": { + "title": "Ошибка проверки", + "description": "Пожалуйста, попробуйте позже" + }, + "initial": { + "title": "Обработка оплаты", + "description": "Пожалуйста, подождите, пока мы обрабатываем вашу оплату" + } + }, + "home": { + "welcome": "Добро пожаловать в BearVPN", + "disconnected": "Не подключено", + "connecting": "Подключение", + "connected": "Подключено", + "disconnecting": "Отключение", + "currentConnectionTitle": "Текущее подключение", + "switchNode": "Сменить узел", + "timeout": "Тайм-аут", + "loading": "Загрузка...", + "error": "Ошибка загрузки", + "checkNetwork": "Проверьте подключение к сети и повторите попытку", + "retry": "Повторить", + "connectionSectionTitle": "Способ подключения", + "dedicatedServers": "Выделенные серверы", + "countryRegion": "Страна/Регион", + "serverListTitle": "Группы выделенных серверов", + "nodeListTitle": "Все узлы", + "countryListTitle": "Список стран/регионов", + "noServers": "Нет доступных серверов", + "noNodes": "Нет доступных узлов", + "noRegions": "Нет доступных регионов", + "subscriptionDescription": "Подпишитесь для доступа к глобальной высокоскоростной сети", + "subscribe": "Подписаться сейчас", + "trialPeriod": "Добро пожаловать в пробную версию Premium", + "remainingTime": "Осталось времени", + "trialExpired": "Пробный период истёк, соединение разорвано", + "subscriptionExpired": "Подписка истекла, соединение разорвано", + "subscriptionUpdated": "Подписка обновлена", + "subscriptionUpdatedMessage": "Ваша информация о подписке обновлена, пожалуйста, обновите страницу для просмотра последнего статуса", + "trialStatus": "Статус пробного периода", + "trialing": "Пробный период", + "trialEndMessage": "После окончания пробного периода сервис будет недоступен", + "lastDaySubscriptionStatus": "Подписка скоро истекает", + "lastDaySubscriptionMessage": "Скоро истекает", + "subscriptionEndMessage": "После окончания подписки сервис будет недоступен", + "trialTimeWithDays": "{days}д {hours}ч {minutes}м {seconds}с", + "trialTimeWithHours": "{hours}ч {minutes}м {seconds}с", + "trialTimeWithMinutes": "{minutes}м {seconds}с", + "testLatency": "Тест задержки", + "testing": "Тестирование задержки", + "refreshLatency": "Обновить задержку", + "refreshLatencyDesc": "Обновить задержку всех узлов", + "testAllNodesLatency": "Тест сетевой задержки всех узлов", + "autoSelect": "Автоматический выбор", + "selected": "Выбрано" + }, + "dialog": { + "confirm": "Подтвердить", + "cancel": "Отмена", + "ok": "OK", + "iKnow": "Я понял" + }, + "update": { + "title": "Доступна новая версия", + "content": "Хотите обновить сейчас?", + "updateNow": "Обновить сейчас", + "later": "Позже", + "defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта" + }, + "country": { + "cn": "Китай", + "ir": "Иран", + "af": "Афганистан", + "ru": "Россия", + "id": "Индонезия", + "tr": "Турция", + "br": "Бразилия" + }, + "error": { + "200": "Успех", + "500": "Внутренняя ошибка сервера", + "10001": "Ошибка запроса базы данных", + "10002": "Ошибка обновления базы данных", + "10003": "Ошибка вставки в базу данных", + "10004": "Ошибка удаления из базы данных", + "20001": "Пользователь уже существует", + "20002": "Пользователь не существует", + "20003": "Ошибка пароля пользователя", + "20004": "Пользователь отключен", + "20005": "Недостаточно средств", + "20006": "Регистрация остановлена", + "20007": "Telegram не привязан", + "20008": "Пользователь не привязал метод OAuth", + "20009": "Ошибка кода приглашения", + "30001": "Узел уже существует", + "30002": "Узел не существует", + "30003": "Группа узлов уже существует", + "30004": "Группа узлов не существует", + "30005": "Группа узлов не пуста", + "400": "Ошибка параметра", + "40002": "Токен пользователя пуст", + "40003": "Токен пользователя недействителен", + "40004": "Срок действия токена пользователя истек", + "40005": "Недопустимый доступ", + "401": "Слишком много запросов", + "50001": "Купон не существует", + "50002": "Купон был использован", + "50003": "Купон не совпадает", + "60001": "Подписка истекла", + "60002": "Подписка недоступна", + "60003": "У пользователя уже есть подписка", + "60004": "Подписка уже использована", + "60005": "Режим одной подписки превышает лимит", + "60006": "Лимит квоты подписки", + "70001": "Ошибка кода подтверждения", + "80001": "Ошибка постановки в очередь", + "90001": "Режим отладки включен", + "90002": "Ошибка отправки SMS", + "90003": "SMS не включены", + "90004": "Электронная почта не включена", + "90005": "Неподдерживаемый метод входа", + "90006": "Аутентификатор не поддерживает этот метод", + "90007": "Код телефонного региона пуст", + "90008": "Пароль пуст", + "90009": "Код региона пуст", + "90010": "Требуется пароль или код подтверждения", + "90011": "Электронная почта уже существует", + "90012": "Телефон уже существует", + "90013": "Устройство уже существует", + "90014": "Ошибка номера телефона", + "90015": "Этот аккаунт достиг лимита отправки на сегодня", + "90017": "Устройство не существует", + "90018": "ID пользователя не совпадает", + "61001": "Заказ не существует", + "61002": "Способ оплаты не найден", + "61003": "Ошибка статуса заказа", + "61004": "Недостаточный период сброса", + "61005": "Существует неиспользованный трафик" + }, + "tray": { + "open_dashboard": "Открыть панель", + "copy_to_terminal": "Копировать в терминал", + "exit_app": "Выйти из приложения" + }, + "splash": { + "appName": "BearVPN", + "slogan": "Высокоскоростная глобальная сеть", + "initializing": "Инициализация...", + "networkConnectionFailure": "Ошибка подключения к сети, проверьте и повторите попытку", + "retry": "Повторить", + "networkPermissionFailed": "Не удалось получить разрешение на использование сети", + "initializationFailed": "Ошибка инициализации" + }, + "network": { + "status": { + "connected": "Подключено", + "disconnected": "Отключено", + "connecting": "Подключение...", + "disconnecting": "Отключение...", + "reconnecting": "Переподключение...", + "failed": "Ошибка подключения" + }, + "permission": { + "title": "Разрешение сети", + "description": "Требуется разрешение на использование сети для предоставления VPN-сервиса", + "goToSettings": "Перейти в настройки", + "cancel": "Отмена" + } + } +} \ No newline at end of file diff --git a/assets/translations/strings_zh.i18n.json b/assets/translations/strings_zh.i18n.json index 1be4057..5aa3632 100755 --- a/assets/translations/strings_zh.i18n.json +++ b/assets/translations/strings_zh.i18n.json @@ -53,6 +53,7 @@ "codeSent": "已向 {account} 发送6位数代码。请在接下来的 30 分钟内输入。", "back": "返回", "enterEmailOrPhone": "输入邮箱或者手机号", + "enterEmail": "请输入电子邮箱", "enterCode": "请输入验证码", "enterPassword": "请输入密码", "reenterPassword": "请再次输入密码", diff --git a/assets/translations/strings_zh_Hant.i18n.json b/assets/translations/strings_zh_Hant.i18n.json index 1ef4300..4e91a34 100755 --- a/assets/translations/strings_zh_Hant.i18n.json +++ b/assets/translations/strings_zh_Hant.i18n.json @@ -6,6 +6,7 @@ "codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。", "back": "返回", "enterEmailOrPhone": "輸入郵箱或者手機號", + "enterEmail": "請輸入電子郵箱", "enterCode": "請輸入驗證碼", "enterPassword": "請輸入密碼", "reenterPassword": "請再次輸入密碼", diff --git a/assets/translations/strings_zh_Hant.i18n.json.bak b/assets/translations/strings_zh_Hant.i18n.json.bak new file mode 100755 index 0000000..1ef4300 --- /dev/null +++ b/assets/translations/strings_zh_Hant.i18n.json.bak @@ -0,0 +1,426 @@ +{ + "login": { + "welcome": "歡迎使用 BearVPN!", + "verifyPhone": "驗證您的手機號", + "verifyEmail": "驗證您的郵箱", + "codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。", + "back": "返回", + "enterEmailOrPhone": "輸入郵箱或者手機號", + "enterCode": "請輸入驗證碼", + "enterPassword": "請輸入密碼", + "reenterPassword": "請再次輸入密碼", + "forgotPassword": "忘記密碼", + "codeLogin": "驗證碼登錄", + "passwordLogin": "密碼登錄", + "agreeTerms": "登錄/創建賬戶,即表示我同意", + "termsOfService": "服務條款", + "privacyPolicy": "隱私政策", + "next": "下一步", + "registerNow": "立即註冊", + "setAndLogin": "設置並登錄", + "enterAccount": "請輸入賬戶", + "passwordMismatch": "兩次密碼輸入不一致", + "sendCode": "發送驗證碼", + "codeSentCountdown": "驗證碼已發送 {seconds}s", + "and": "和", + "enterInviteCode": "請輸入邀請碼(選填)", + "registerSuccess": "註冊成功" + }, + "failure": { + "unexpected": "意外錯誤", + "clash": { + "unexpected": "意外錯誤", + "core": "Clash 錯誤 ${reason}" + }, + "singbox": { + "unexpected": "意外服務錯誤", + "serviceNotRunning": "服務未運行", + "missingPrivilege": "缺少權限", + "missingPrivilegeMsg": "VPN 模式需要管理員權限。以管理員身份重新啟動應用程序或更改服務模式", + "missingGeoAssets": "缺失 GEO 資源文件", + "missingGeoAssetsMsg": "缺失 GEO 資源文件。請考慮更改激活的資源文件或在設置中下載所選資源文件。", + "invalidConfigOptions": "配置選項無效", + "invalidConfig": "無效配置", + "create": "服務創建錯誤", + "start": "服務啟動錯誤" + }, + "connectivity": { + "unexpected": "意外失敗", + "missingVpnPermission": "缺少 VPN 權限", + "missingNotificationPermission": "缺少通知權限", + "core": "核心錯誤" + }, + "profiles": { + "unexpected": "意外錯誤", + "notFound": "未找到配置文件", + "invalidConfig": "無效配置", + "invalidUrl": "網址無效" + }, + "connection": { + "unexpected": "意外連接錯誤", + "timeout": "連接超時", + "badResponse": "錯誤響應", + "connectionError": "連接錯誤", + "badCertificate": "證書無效" + }, + "geoAssets": { + "unexpected": "意外錯誤", + "notUpdate": "無可用更新", + "activeNotFound": "未找到激活的 GEO 資源文件" + } + }, + "userInfo": { + "title": "我的信息", + "bindingTip": "未綁定郵箱/手機號", + "myAccount": "我的賬號", + "balance": "餘額", + "noValidSubscription": "您沒有有效的訂閱", + "subscribeNow": "立即訂閱", + "shortcuts": "快捷鍵", + "adBlock": "廣告攔截", + "dnsUnlock": "DNS 解鎖", + "contactUs": "聯繫我們", + "others": "其他", + "logout": "退出登錄", + "logoutConfirmTitle": "退出登錄", + "logoutConfirmMessage": "確定要退出登錄嗎?", + "logoutCancel": "取消", + "vpnWebsite": "VPN 官網", + "telegram": "Telegram", + "mail": "郵箱", + "phone": "電話", + "customerService": "人工客服", + "workOrder": "填寫工單", + "pleaseLogin": "請先登錄賬號", + "subscriptionValid": "訂閱有效", + "startTime": "開始時間:", + "expireTime": "到期時間:", + "loginNow": "立即登錄", + "trialPeriod": "歡迎使用高級試用版", + "remainingTime": "剩餘時間", + "trialExpired": "試用期已結束,連接已斷開", + "subscriptionExpired": "訂閱已過期,連接已斷開", + "copySuccess": "複製成功", + "notAvailable": "暫無", + "willBeDeleted": "將被刪除", + "deleteAccountWarning": "賬號刪除是永久性的。一旦您的賬號被刪除,您將無法使用任何功能。是否繼續?", + "requestDelete": "請求刪除", + "switchSubscription": "切換訂閱", + "resetTrafficTitle": "重置流量", + "resetTrafficMessage": "月付套餐流量重置示例:將下一個週期的流量按月重置,訂閱有效期將從{currentTime}提前至{newTime}", + "reset": "重置", + "trafficUsage": "已用: {used} / {total}", + "trafficProgress": { + "title": "流量使用情況", + "unlimited": "不限流量", + "limited": "已用流量" + }, + "deviceLimit": "設備限制: {count}" + }, + "setting": { + "title": "設置", + "vpnConnection": "VPN連接", + "general": "通用", + "autoConnect": "自動連接", + "routeRule": "路由規則", + "countrySelector": "選擇國家", + "appearance": "外觀", + "notifications": "通知", + "helpImprove": "幫助我們改進", + "helpImproveSubtitle": "幫助我們改進的副標題", + "requestDeleteAccount": "請求刪除賬號", + "goToDelete": "去刪除", + "rateUs": "在 App Store 上為我們評分", + "iosRating": "iOS評分", + "version": "版本", + "switchLanguage": "切換語言", + "system": "系統", + "light": "淺色", + "dark": "深色", + "vpnModeSmart": "智能模式", + "mode": "出站模式", + "connectionTypeGlobal": "全域代理", + "connectionTypeGlobalRemark": "啟用後,所有流量均通過代理伺服器轉發", + "connectionTypeRule": "智能代理", + "connectionTypeRuleRemark": "當[出站模式]設置為[智能代理]時,根據所選國家,系統自動分流:國內IP/域名直連,境外請求透過代理訪問", + "connectionTypeDirect": "直連", + "connectionTypeDirectRemark": "啟用後,所有流量均不經代理直接訪問", + "smartMode": "智能模式", + "secureMode": "安全模式", + "deviceLimit": "設備限制: {count}" + }, + "statistics": { + "title": "統計", + "vpnStatus": "VPN 狀態", + "ipAddress": "IP地址", + "connectionTime": "連接時間", + "protocol": "協議", + "weeklyProtectionTime": "每週保護時間", + "currentStreak": "當前連續記錄", + "highestStreak": "最高記錄", + "longestConnection": "最長連接時間", + "days": "{days}天", + "daysOfWeek": { + "monday": "週\n一", + "tuesday": "週\n二", + "wednesday": "週\n三", + "thursday": "週\n四", + "friday": "週\n五", + "saturday": "週\n六", + "sunday": "週\n日" + } + }, + "message": { + "title": "通知", + "system": "系統消息", + "promotion": "促銷消息" + }, + "invite": { + "title": "邀請好友", + "progress": "邀請進度", + "inviteStats": "邀請統計", + "registers": "已註冊", + "totalCommission": "總佣金", + "rewardDetails": "獎勵明細 >", + "steps": "邀請步驟", + "inviteFriend": "邀請好友", + "acceptInvite": "好友接受邀請\n下單並註冊", + "getReward": "獲得獎勵", + "shareLink": "分享連結", + "shareQR": "分享二維碼", + "rules": "邀請規則", + "rule1": "1、您可以通過分享專屬邀請連結或邀請碼給好友,邀請他們加入我們。", + "rule2": "2、好友完成註冊並登錄後,邀請獎勵將自動發放至您的賬戶。", + "pending": "待下載", + "processing": "在路上", + "success": "已成功", + "expired": "已失效", + "myInviteCode": "我的邀請碼", + "inviteCodeCopied": "邀請碼已複製到剪貼板", + "close": "關閉", + "saveQRCode": "保存二維碼", + "qrCodeSaved": "二維碼已保存", + "copiedToClipboard": "已複製到剪貼板", + "getInviteCodeFailed": "獲取邀請碼失敗,請稍後重試", + "generateQRCodeFailed": "生成二維碼失敗,請稍後重試", + "generateShareLinkFailed": "生成分享連結失敗,請稍後重試" + }, + "purchaseMembership": { + "purchasePackage": "購買套餐", + "noData": "暫無可用套餐", + "myAccount": "我的賬號", + "selectPackage": "選擇套餐", + "packageDescription": "套餐描述", + "paymentMethod": "支付方式", + "cancelAnytime": "您可以隨時在APP上取消", + "startSubscription": "開始訂閱", + "renewNow": "立即續訂", + "month": "{months}個月", + "year": "{years}年", + "day": "{days}天", + "unlimitedTraffic": "不限流量", + "unlimitedDevices": "不限設備", + "devices": "{count}台", + "trafficLimit": "流量限制", + "deviceLimit": "設備限制", + "features": "套餐特性", + "expand": "展開", + "collapse": "收起", + "confirmPurchase": "確認購買", + "confirmPurchaseDesc": "您確定要購買此套餐嗎?" + }, + "home": { + "welcome": "歡迎使用 BearVPN", + "disconnected": "已斷開連接", + "connecting": "正在連接", + "connected": "已連接", + "disconnecting": "正在斷開連接", + "currentConnectionTitle": "當前連接", + "switchNode": "切換節點", + "timeout": "超時", + "loading": "載入中...", + "error": "載入失敗", + "checkNetwork": "請檢查網絡連接並重試", + "retry": "重試", + "connectionSectionTitle": "連接方式", + "dedicatedServers": "專用伺服器", + "countryRegion": "國家/地區", + "serverListTitle": "專用伺服器群組", + "nodeListTitle": "所有節點", + "countryListTitle": "國家/地區列表", + "noServers": "暫無可用伺服器", + "noNodes": "暫無可用節點", + "noRegions": "暫無可用地區", + "subscriptionDescription": "訂閱會員,暢享全球高速網絡", + "subscribe": "立即訂閱", + "trialPeriod": "歡迎使用 Premium 試用版", + "remainingTime": "剩餘時間", + "trialExpired": "試用期已結束,已斷開連接", + "subscriptionExpired": "訂閱已過期,已斷開連接", + "subscriptionUpdated": "訂閱已更新", + "subscriptionUpdatedMessage": "您的訂閱信息已更新,請刷新頁面查看最新狀態", + "trialStatus": "試用狀態", + "trialing": "試用中", + "trialEndMessage": "試用期結束後將無法使用", + "lastDaySubscriptionStatus": "訂閱即將到期", + "lastDaySubscriptionMessage": "即將到期", + "subscriptionEndMessage": "訂閱到期後將無法使用", + "trialTimeWithDays": "{days}天 {hours}時 {minutes}分 {seconds}秒", + "trialTimeWithHours": "{hours}時 {minutes}分 {seconds}秒", + "trialTimeWithMinutes": "{minutes}分 {seconds}秒", + "refreshLatency": "刷新延遲", + "testLatency": "測試延遲", + "testing": "正在測試延遲", + "refreshLatencyDesc": "刷新所有節點的延遲", + "testAllNodesLatency": "測試所有節點的網絡延遲", + "autoSelect": "自動選擇", + "selected": "已選擇" + }, + "dialog": { + "confirm": "確認", + "cancel": "取消", + "ok": "我知道了" + }, + "update": { + "title": "發現新版本", + "content": "是否立即更新?", + "updateNow": "立即更新", + "updateLater": "稍後", + "defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗" + }, + "orderStatus": { + "title": "訂單狀態", + "pending": { + "title": "待支付", + "description": "請完成支付" + }, + "paid": { + "title": "已支付", + "description": "正在處理您的訂單" + }, + "success": { + "title": "恭喜你!支付成功", + "description": "您的套餐已經購買成功了" + }, + "closed": { + "title": "訂單已關閉", + "description": "請重新下單" + }, + "failed": { + "title": "支付失敗", + "description": "請重新嘗試支付" + }, + "unknown": { + "title": "未知狀態", + "description": "請聯繫客服" + }, + "checkFailed": { + "title": "檢查失敗", + "description": "請稍後重試" + }, + "initial": { + "title": "支付中", + "description": "請稍候,正在處理您的支付" + } + }, + "country": { + "cn": "中國", + "ir": "伊朗", + "af": "阿富汗", + "ru": "俄羅斯", + "id": "印尼", + "tr": "土耳其", + "br": "巴西" + }, + "error": { + "200": "成功", + "500": "服務器內部錯誤", + "10001": "數據庫查詢錯誤", + "10002": "數據庫更新錯誤", + "10003": "數據庫插入錯誤", + "10004": "數據庫刪除錯誤", + "20001": "用戶已存在", + "20002": "用戶不存在", + "20003": "用戶密碼錯誤", + "20004": "用戶已被禁用", + "20005": "餘額不足", + "20006": "註冊已暫停", + "20007": "未綁定 Telegram", + "20008": "用戶未綁定 OAuth", + "20009": "邀請碼錯誤", + "30001": "節點已存在", + "30002": "節點不存在", + "30003": "節點群組已存在", + "30004": "節點群組不存在", + "30005": "節點群組不為空", + "400": "參數錯誤", + "40002": "用戶令牌為空", + "40003": "用戶令牌無效", + "40004": "用戶令牌已過期", + "40005": "未登錄", + "401": "請求過多", + "50001": "優惠券不存在", + "50002": "優惠券已使用", + "50003": "優惠券不匹配", + "60001": "訂閱已過期", + "60002": "訂閱不可用", + "60003": "用戶已有訂閱", + "60004": "訂閱已使用", + "60005": "單次訂閱模式超出限制", + "60006": "訂閱配額限制", + "70001": "驗證碼錯誤", + "80001": "加入隊列錯誤", + "90001": "調試模式已啟用", + "90002": "發送短信錯誤", + "90003": "短信功能未啟用", + "90004": "郵件功能未啟用", + "90005": "不支持的登錄方式", + "90006": "驗證器不支持此方式", + "90007": "電話國家代碼為空", + "90008": "密碼為空", + "90009": "國家代碼為空", + "90010": "需要密碼或驗證碼", + "90011": "郵箱已存在", + "90012": "手機號已存在", + "90013": "設備已存在", + "90014": "手機號錯誤", + "90015": "該賬號今日已達到發送限制", + "90017": "設備不存在", + "90018": "用戶 ID 不匹配", + "61001": "訂閱不存在", + "61002": "未找到支付方式", + "61003": "訂閱狀態錯誤", + "61004": "重置期不足", + "61005": "存在未使用流量" + }, + "tray": { + "open_dashboard": "打開面板", + "copy_to_terminal": "複製到終端", + "exit_app": "退出應用" + }, + "splash": { + "appName": "BearVPN", + "slogan": "暢享全球高速網絡", + "initializing": "正在初始化...", + "networkConnectionFailure": "網絡連接失敗,請檢查並重試", + "retry": "重試", + "networkPermissionFailed": "獲取網絡權限失敗", + "initializationFailed": "初始化失敗" + }, + "network": { + "status": { + "connected": "已連接", + "disconnected": "已斷開連接", + "connecting": "正在連接...", + "disconnecting": "正在斷開連接...", + "reconnecting": "正在重新連接...", + "failed": "連接失敗" + }, + "permission": { + "title": "網絡權限", + "description": "需要網絡權限以提供 VPN 服務", + "goToSettings": "前往設置", + "cancel": "取消" + } + } +} \ No newline at end of file diff --git a/assets/translations/strings_zh_Hant.i18n.json.bak2 b/assets/translations/strings_zh_Hant.i18n.json.bak2 new file mode 100755 index 0000000..5cdac91 --- /dev/null +++ b/assets/translations/strings_zh_Hant.i18n.json.bak2 @@ -0,0 +1,427 @@ +{ + "login": { + "welcome": "歡迎使用 BearVPN!", + "verifyPhone": "驗證您的手機號", + "verifyEmail": "驗證您的郵箱", + "codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。", + "back": "返回", + "enterEmailOrPhone": "輸入郵箱或者手機號", + "enterEmail": "Please enter email address", + "enterCode": "請輸入驗證碼", + "enterPassword": "請輸入密碼", + "reenterPassword": "請再次輸入密碼", + "forgotPassword": "忘記密碼", + "codeLogin": "驗證碼登錄", + "passwordLogin": "密碼登錄", + "agreeTerms": "登錄/創建賬戶,即表示我同意", + "termsOfService": "服務條款", + "privacyPolicy": "隱私政策", + "next": "下一步", + "registerNow": "立即註冊", + "setAndLogin": "設置並登錄", + "enterAccount": "請輸入賬戶", + "passwordMismatch": "兩次密碼輸入不一致", + "sendCode": "發送驗證碼", + "codeSentCountdown": "驗證碼已發送 {seconds}s", + "and": "和", + "enterInviteCode": "請輸入邀請碼(選填)", + "registerSuccess": "註冊成功" + }, + "failure": { + "unexpected": "意外錯誤", + "clash": { + "unexpected": "意外錯誤", + "core": "Clash 錯誤 ${reason}" + }, + "singbox": { + "unexpected": "意外服務錯誤", + "serviceNotRunning": "服務未運行", + "missingPrivilege": "缺少權限", + "missingPrivilegeMsg": "VPN 模式需要管理員權限。以管理員身份重新啟動應用程序或更改服務模式", + "missingGeoAssets": "缺失 GEO 資源文件", + "missingGeoAssetsMsg": "缺失 GEO 資源文件。請考慮更改激活的資源文件或在設置中下載所選資源文件。", + "invalidConfigOptions": "配置選項無效", + "invalidConfig": "無效配置", + "create": "服務創建錯誤", + "start": "服務啟動錯誤" + }, + "connectivity": { + "unexpected": "意外失敗", + "missingVpnPermission": "缺少 VPN 權限", + "missingNotificationPermission": "缺少通知權限", + "core": "核心錯誤" + }, + "profiles": { + "unexpected": "意外錯誤", + "notFound": "未找到配置文件", + "invalidConfig": "無效配置", + "invalidUrl": "網址無效" + }, + "connection": { + "unexpected": "意外連接錯誤", + "timeout": "連接超時", + "badResponse": "錯誤響應", + "connectionError": "連接錯誤", + "badCertificate": "證書無效" + }, + "geoAssets": { + "unexpected": "意外錯誤", + "notUpdate": "無可用更新", + "activeNotFound": "未找到激活的 GEO 資源文件" + } + }, + "userInfo": { + "title": "我的信息", + "bindingTip": "未綁定郵箱/手機號", + "myAccount": "我的賬號", + "balance": "餘額", + "noValidSubscription": "您沒有有效的訂閱", + "subscribeNow": "立即訂閱", + "shortcuts": "快捷鍵", + "adBlock": "廣告攔截", + "dnsUnlock": "DNS 解鎖", + "contactUs": "聯繫我們", + "others": "其他", + "logout": "退出登錄", + "logoutConfirmTitle": "退出登錄", + "logoutConfirmMessage": "確定要退出登錄嗎?", + "logoutCancel": "取消", + "vpnWebsite": "VPN 官網", + "telegram": "Telegram", + "mail": "郵箱", + "phone": "電話", + "customerService": "人工客服", + "workOrder": "填寫工單", + "pleaseLogin": "請先登錄賬號", + "subscriptionValid": "訂閱有效", + "startTime": "開始時間:", + "expireTime": "到期時間:", + "loginNow": "立即登錄", + "trialPeriod": "歡迎使用高級試用版", + "remainingTime": "剩餘時間", + "trialExpired": "試用期已結束,連接已斷開", + "subscriptionExpired": "訂閱已過期,連接已斷開", + "copySuccess": "複製成功", + "notAvailable": "暫無", + "willBeDeleted": "將被刪除", + "deleteAccountWarning": "賬號刪除是永久性的。一旦您的賬號被刪除,您將無法使用任何功能。是否繼續?", + "requestDelete": "請求刪除", + "switchSubscription": "切換訂閱", + "resetTrafficTitle": "重置流量", + "resetTrafficMessage": "月付套餐流量重置示例:將下一個週期的流量按月重置,訂閱有效期將從{currentTime}提前至{newTime}", + "reset": "重置", + "trafficUsage": "已用: {used} / {total}", + "trafficProgress": { + "title": "流量使用情況", + "unlimited": "不限流量", + "limited": "已用流量" + }, + "deviceLimit": "設備限制: {count}" + }, + "setting": { + "title": "設置", + "vpnConnection": "VPN連接", + "general": "通用", + "autoConnect": "自動連接", + "routeRule": "路由規則", + "countrySelector": "選擇國家", + "appearance": "外觀", + "notifications": "通知", + "helpImprove": "幫助我們改進", + "helpImproveSubtitle": "幫助我們改進的副標題", + "requestDeleteAccount": "請求刪除賬號", + "goToDelete": "去刪除", + "rateUs": "在 App Store 上為我們評分", + "iosRating": "iOS評分", + "version": "版本", + "switchLanguage": "切換語言", + "system": "系統", + "light": "淺色", + "dark": "深色", + "vpnModeSmart": "智能模式", + "mode": "出站模式", + "connectionTypeGlobal": "全域代理", + "connectionTypeGlobalRemark": "啟用後,所有流量均通過代理伺服器轉發", + "connectionTypeRule": "智能代理", + "connectionTypeRuleRemark": "當[出站模式]設置為[智能代理]時,根據所選國家,系統自動分流:國內IP/域名直連,境外請求透過代理訪問", + "connectionTypeDirect": "直連", + "connectionTypeDirectRemark": "啟用後,所有流量均不經代理直接訪問", + "smartMode": "智能模式", + "secureMode": "安全模式", + "deviceLimit": "設備限制: {count}" + }, + "statistics": { + "title": "統計", + "vpnStatus": "VPN 狀態", + "ipAddress": "IP地址", + "connectionTime": "連接時間", + "protocol": "協議", + "weeklyProtectionTime": "每週保護時間", + "currentStreak": "當前連續記錄", + "highestStreak": "最高記錄", + "longestConnection": "最長連接時間", + "days": "{days}天", + "daysOfWeek": { + "monday": "週\n一", + "tuesday": "週\n二", + "wednesday": "週\n三", + "thursday": "週\n四", + "friday": "週\n五", + "saturday": "週\n六", + "sunday": "週\n日" + } + }, + "message": { + "title": "通知", + "system": "系統消息", + "promotion": "促銷消息" + }, + "invite": { + "title": "邀請好友", + "progress": "邀請進度", + "inviteStats": "邀請統計", + "registers": "已註冊", + "totalCommission": "總佣金", + "rewardDetails": "獎勵明細 >", + "steps": "邀請步驟", + "inviteFriend": "邀請好友", + "acceptInvite": "好友接受邀請\n下單並註冊", + "getReward": "獲得獎勵", + "shareLink": "分享連結", + "shareQR": "分享二維碼", + "rules": "邀請規則", + "rule1": "1、您可以通過分享專屬邀請連結或邀請碼給好友,邀請他們加入我們。", + "rule2": "2、好友完成註冊並登錄後,邀請獎勵將自動發放至您的賬戶。", + "pending": "待下載", + "processing": "在路上", + "success": "已成功", + "expired": "已失效", + "myInviteCode": "我的邀請碼", + "inviteCodeCopied": "邀請碼已複製到剪貼板", + "close": "關閉", + "saveQRCode": "保存二維碼", + "qrCodeSaved": "二維碼已保存", + "copiedToClipboard": "已複製到剪貼板", + "getInviteCodeFailed": "獲取邀請碼失敗,請稍後重試", + "generateQRCodeFailed": "生成二維碼失敗,請稍後重試", + "generateShareLinkFailed": "生成分享連結失敗,請稍後重試" + }, + "purchaseMembership": { + "purchasePackage": "購買套餐", + "noData": "暫無可用套餐", + "myAccount": "我的賬號", + "selectPackage": "選擇套餐", + "packageDescription": "套餐描述", + "paymentMethod": "支付方式", + "cancelAnytime": "您可以隨時在APP上取消", + "startSubscription": "開始訂閱", + "renewNow": "立即續訂", + "month": "{months}個月", + "year": "{years}年", + "day": "{days}天", + "unlimitedTraffic": "不限流量", + "unlimitedDevices": "不限設備", + "devices": "{count}台", + "trafficLimit": "流量限制", + "deviceLimit": "設備限制", + "features": "套餐特性", + "expand": "展開", + "collapse": "收起", + "confirmPurchase": "確認購買", + "confirmPurchaseDesc": "您確定要購買此套餐嗎?" + }, + "home": { + "welcome": "歡迎使用 BearVPN", + "disconnected": "已斷開連接", + "connecting": "正在連接", + "connected": "已連接", + "disconnecting": "正在斷開連接", + "currentConnectionTitle": "當前連接", + "switchNode": "切換節點", + "timeout": "超時", + "loading": "載入中...", + "error": "載入失敗", + "checkNetwork": "請檢查網絡連接並重試", + "retry": "重試", + "connectionSectionTitle": "連接方式", + "dedicatedServers": "專用伺服器", + "countryRegion": "國家/地區", + "serverListTitle": "專用伺服器群組", + "nodeListTitle": "所有節點", + "countryListTitle": "國家/地區列表", + "noServers": "暫無可用伺服器", + "noNodes": "暫無可用節點", + "noRegions": "暫無可用地區", + "subscriptionDescription": "訂閱會員,暢享全球高速網絡", + "subscribe": "立即訂閱", + "trialPeriod": "歡迎使用 Premium 試用版", + "remainingTime": "剩餘時間", + "trialExpired": "試用期已結束,已斷開連接", + "subscriptionExpired": "訂閱已過期,已斷開連接", + "subscriptionUpdated": "訂閱已更新", + "subscriptionUpdatedMessage": "您的訂閱信息已更新,請刷新頁面查看最新狀態", + "trialStatus": "試用狀態", + "trialing": "試用中", + "trialEndMessage": "試用期結束後將無法使用", + "lastDaySubscriptionStatus": "訂閱即將到期", + "lastDaySubscriptionMessage": "即將到期", + "subscriptionEndMessage": "訂閱到期後將無法使用", + "trialTimeWithDays": "{days}天 {hours}時 {minutes}分 {seconds}秒", + "trialTimeWithHours": "{hours}時 {minutes}分 {seconds}秒", + "trialTimeWithMinutes": "{minutes}分 {seconds}秒", + "refreshLatency": "刷新延遲", + "testLatency": "測試延遲", + "testing": "正在測試延遲", + "refreshLatencyDesc": "刷新所有節點的延遲", + "testAllNodesLatency": "測試所有節點的網絡延遲", + "autoSelect": "自動選擇", + "selected": "已選擇" + }, + "dialog": { + "confirm": "確認", + "cancel": "取消", + "ok": "我知道了" + }, + "update": { + "title": "發現新版本", + "content": "是否立即更新?", + "updateNow": "立即更新", + "updateLater": "稍後", + "defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗" + }, + "orderStatus": { + "title": "訂單狀態", + "pending": { + "title": "待支付", + "description": "請完成支付" + }, + "paid": { + "title": "已支付", + "description": "正在處理您的訂單" + }, + "success": { + "title": "恭喜你!支付成功", + "description": "您的套餐已經購買成功了" + }, + "closed": { + "title": "訂單已關閉", + "description": "請重新下單" + }, + "failed": { + "title": "支付失敗", + "description": "請重新嘗試支付" + }, + "unknown": { + "title": "未知狀態", + "description": "請聯繫客服" + }, + "checkFailed": { + "title": "檢查失敗", + "description": "請稍後重試" + }, + "initial": { + "title": "支付中", + "description": "請稍候,正在處理您的支付" + } + }, + "country": { + "cn": "中國", + "ir": "伊朗", + "af": "阿富汗", + "ru": "俄羅斯", + "id": "印尼", + "tr": "土耳其", + "br": "巴西" + }, + "error": { + "200": "成功", + "500": "服務器內部錯誤", + "10001": "數據庫查詢錯誤", + "10002": "數據庫更新錯誤", + "10003": "數據庫插入錯誤", + "10004": "數據庫刪除錯誤", + "20001": "用戶已存在", + "20002": "用戶不存在", + "20003": "用戶密碼錯誤", + "20004": "用戶已被禁用", + "20005": "餘額不足", + "20006": "註冊已暫停", + "20007": "未綁定 Telegram", + "20008": "用戶未綁定 OAuth", + "20009": "邀請碼錯誤", + "30001": "節點已存在", + "30002": "節點不存在", + "30003": "節點群組已存在", + "30004": "節點群組不存在", + "30005": "節點群組不為空", + "400": "參數錯誤", + "40002": "用戶令牌為空", + "40003": "用戶令牌無效", + "40004": "用戶令牌已過期", + "40005": "未登錄", + "401": "請求過多", + "50001": "優惠券不存在", + "50002": "優惠券已使用", + "50003": "優惠券不匹配", + "60001": "訂閱已過期", + "60002": "訂閱不可用", + "60003": "用戶已有訂閱", + "60004": "訂閱已使用", + "60005": "單次訂閱模式超出限制", + "60006": "訂閱配額限制", + "70001": "驗證碼錯誤", + "80001": "加入隊列錯誤", + "90001": "調試模式已啟用", + "90002": "發送短信錯誤", + "90003": "短信功能未啟用", + "90004": "郵件功能未啟用", + "90005": "不支持的登錄方式", + "90006": "驗證器不支持此方式", + "90007": "電話國家代碼為空", + "90008": "密碼為空", + "90009": "國家代碼為空", + "90010": "需要密碼或驗證碼", + "90011": "郵箱已存在", + "90012": "手機號已存在", + "90013": "設備已存在", + "90014": "手機號錯誤", + "90015": "該賬號今日已達到發送限制", + "90017": "設備不存在", + "90018": "用戶 ID 不匹配", + "61001": "訂閱不存在", + "61002": "未找到支付方式", + "61003": "訂閱狀態錯誤", + "61004": "重置期不足", + "61005": "存在未使用流量" + }, + "tray": { + "open_dashboard": "打開面板", + "copy_to_terminal": "複製到終端", + "exit_app": "退出應用" + }, + "splash": { + "appName": "BearVPN", + "slogan": "暢享全球高速網絡", + "initializing": "正在初始化...", + "networkConnectionFailure": "網絡連接失敗,請檢查並重試", + "retry": "重試", + "networkPermissionFailed": "獲取網絡權限失敗", + "initializationFailed": "初始化失敗" + }, + "network": { + "status": { + "connected": "已連接", + "disconnected": "已斷開連接", + "connecting": "正在連接...", + "disconnecting": "正在斷開連接...", + "reconnecting": "正在重新連接...", + "failed": "連接失敗" + }, + "permission": { + "title": "網絡權限", + "description": "需要網絡權限以提供 VPN 服務", + "goToSettings": "前往設置", + "cancel": "取消" + } + } +} \ No newline at end of file diff --git a/lib/app/common/app_config.dart b/lib/app/common/app_config.dart index 318117c..53cdf51 100755 --- a/lib/app/common/app_config.dart +++ b/lib/app/common/app_config.dart @@ -1071,6 +1071,21 @@ class AppConfig { /// 网站ID String kr_website_id = ""; + /// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + /// 用户信息固定值(临时) + /// ⚠️ 注意:新版本后端已废弃 /v1/app/user/info 接口 + /// 等待新接口实现后,这些值应该从新接口动态获取 + /// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + /// 用户余额(单位:分) + /// 临时固定值:0,表示0.00元 + static const int kr_userBalance = 0; + + /// 用户邀请码 + /// 临时固定值:空字符串,待新接口实现 + static const String kr_userReferCode = ""; + + /// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /// 是否为白天模式 bool kr_is_daytime = true; diff --git a/lib/app/common/app_run_data.dart b/lib/app/common/app_run_data.dart index 81cb744..19002f1 100755 --- a/lib/app/common/app_run_data.dart +++ b/lib/app/common/app_run_data.dart @@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/utils/kr_device_util.dart'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; import '../services/api_service/kr_api.user.dart'; +import '../services/kr_announcement_service.dart'; import '../utils/kr_event_bus.dart'; class KRAppRunData { @@ -23,11 +24,11 @@ class KRAppRunData { /// 登录token String? kr_token; - /// 用户账号 - String? kr_account; + /// 用户账号(使用响应式变量以便 UI 能监听变化) + final Rx kr_account = Rx(null); - /// 用户ID - String? kr_userId; + /// 用户ID(使用响应式变量以便 UI 能监听变化) + final Rx kr_userId = Rx(null); /// 登录类型 KRLoginType? kr_loginType; @@ -46,17 +47,77 @@ class KRAppRunData { return _instance; } + /// 判断是否是设备登录(游客模式) + bool isDeviceLogin() { + // 设备登录的账号格式为 "device_设备ID" + return kr_account.value != null && kr_account.value!.startsWith('device_'); + } + + /// 从JWT token中解析userId + int? _kr_parseUserIdFromToken(String token) { + try { + // JWT格式: header.payload.signature + final parts = token.split('.'); + if (parts.length != 3) { + KRLogUtil.kr_e('JWT token格式错误', tag: 'AppRunData'); + return null; + } + + // 解码payload部分(base64) + String payload = parts[1]; + // 手动添加必要的padding(base64要求长度是4的倍数) + switch (payload.length % 4) { + case 0: + break; // 不需要padding + case 2: + payload += '=='; + break; + case 3: + payload += '='; + break; + default: + KRLogUtil.kr_e('JWT payload长度无效', tag: 'AppRunData'); + return null; + } + + final decodedBytes = base64.decode(payload); + final decodedString = utf8.decode(decodedBytes); + + // 解析JSON + final Map payloadMap = jsonDecode(decodedString); + + // 获取UserId + if (payloadMap.containsKey('UserId')) { + final userId = payloadMap['UserId']; + KRLogUtil.kr_i('从JWT解析出userId: $userId', tag: 'AppRunData'); + return userId is int ? userId : int.tryParse(userId.toString()); + } + + return null; + } catch (e) { + KRLogUtil.kr_e('解析JWT token失败: $e', tag: 'AppRunData'); + return null; + } + } + /// 保存用户信息 Future kr_saveUserInfo( - String token, String account, KRLoginType loginType, String? areaCode) async { + String token, + String account, + KRLoginType loginType, + String? areaCode) async { KRLogUtil.kr_i('开始保存用户信息', tag: 'AppRunData'); - + try { // 更新内存中的数据 kr_token = token; - kr_account = account; + kr_account.value = account; kr_loginType = loginType; kr_areaCode = areaCode; + + // 从JWT token中解析userId + kr_userId.value = _kr_parseUserIdFromToken(token); + KRLogUtil.kr_i('从JWT解析userId: ${kr_userId.value}', tag: 'AppRunData'); final Map userInfo = { 'token': token, @@ -81,15 +142,13 @@ class KRAppRunData { } KRLogUtil.kr_i('用户信息保存成功,设置登录状态为true', tag: 'AppRunData'); - + // 只有在保存成功后才设置登录状态 kr_isLogin.value = true; - // 异步获取用户信息并建立 Socket 连接,不等待结果 - _iniUserInfo().catchError((error) { - KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData'); - // 即使获取用户信息失败,也保持登录状态 - }); + // 设备登录模式不再调用用户信息接口 + // Socket 连接将在需要时建立 + KRLogUtil.kr_i('用户信息已保存,跳过用户信息接口调用', tag: 'AppRunData'); } catch (e) { KRLogUtil.kr_e('保存用户信息失败: $e', tag: 'AppRunData'); @@ -103,20 +162,23 @@ class KRAppRunData { Future kr_loginOut() async { // 先将登录状态设置为 false,防止重连 kr_isLogin.value = false; - + // 断开 Socket 连接 await _kr_disconnectSocket(); - + // 清理用户信息 kr_token = null; - kr_account = null; - kr_userId = null; + kr_account.value = null; + kr_userId.value = null; kr_loginType = null; kr_areaCode = null; // 删除存储的用户信息 await KRSecureStorage().kr_deleteData(key: _keyUserInfo); + // 重置公告显示状态 + KRAnnouncementService().kr_reset(); + // 重置主页面 Get.find().kr_setPage(0); } @@ -135,7 +197,7 @@ class KRAppRunData { try { final Map userInfo = jsonDecode(userInfoString); kr_token = userInfo['token']; - kr_account = userInfo['account']; + kr_account.value = userInfo['account']; final loginTypeValue = userInfo['loginType']; kr_loginType = KRLoginType.values.firstWhere( (e) => e.value == loginTypeValue, @@ -143,18 +205,21 @@ class KRAppRunData { ); kr_areaCode = userInfo['areaCode'] ?? ""; - KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=$kr_account', tag: 'AppRunData'); + // 从token中解析userId + if (kr_token != null && kr_token!.isNotEmpty) { + kr_userId.value = _kr_parseUserIdFromToken(kr_token!); + } + + KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=${kr_account.value}', tag: 'AppRunData'); // 验证token有效性 if (kr_token != null && kr_token!.isNotEmpty) { KRLogUtil.kr_i('设置登录状态为true', tag: 'AppRunData'); kr_isLogin.value = true; - - // 异步获取用户信息,但不等待结果 - _iniUserInfo().catchError((error) { - KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData'); - // 如果获取用户信息失败,不重置登录状态,让用户重试 - }); + + // 设备登录模式不需要调用用户信息接口 + // 用户ID将从订阅信息或其他途径获取 + KRLogUtil.kr_i('已登录,跳过用户信息接口调用', tag: 'AppRunData'); } else { KRLogUtil.kr_w('Token为空,设置为未登录状态', tag: 'AppRunData'); kr_isLogin.value = false; @@ -175,20 +240,6 @@ class KRAppRunData { KRLogUtil.kr_i('用户信息初始化完成,登录状态: ${kr_isLogin.value}', tag: 'AppRunData'); } - /// 初始化用户信息并建立 Socket 连接 - Future _iniUserInfo() async { - final either0 = await KRUserApi().kr_getUserInfo(); - either0.fold( - (error) { - KRLogUtil.kr_e(error.msg, tag: 'AppRunData'); - }, - (userInfo) async { - kr_userId = userInfo.id.toString(); - _kr_connectSocket(kr_userId!); - }, - ); - } - /// 建立 Socket 连接 Future _kr_connectSocket(String userId) async { // 如果已存在连接,先断开 diff --git a/lib/app/localization/app_translations.dart b/lib/app/localization/app_translations.dart index fe8fbf1..50f15f4 100755 --- a/lib/app/localization/app_translations.dart +++ b/lib/app/localization/app_translations.dart @@ -123,6 +123,9 @@ class AppTranslationsLogin { /// 输入邮箱或手机号提示 String get enterEmailOrPhone => 'login.enterEmailOrPhone'.tr; + /// 输入邮箱提示 + String get enterEmail => 'login.enterEmail'.tr; + /// 输入验证码提示 String get enterCode => 'login.enterCode'.tr; diff --git a/lib/app/model/business/kr_outbound_item.dart b/lib/app/model/business/kr_outbound_item.dart index bdf3e82..63131b2 100755 --- a/lib/app/model/business/kr_outbound_item.dart +++ b/lib/app/model/business/kr_outbound_item.dart @@ -45,7 +45,24 @@ class KROutboundItem { city = nodeListItem.city; // 设置城市 country = nodeListItem.country; // 设置国家 - final json = jsonDecode(nodeListItem.config); + // 安全解析 config 字段 + // 新API格式:config为空,直接使用节点字段构建配置 + // 旧API格式:config包含JSON配置 + if (nodeListItem.config.isEmpty) { + print('ℹ️ 节点 ${nodeListItem.name} 使用直接字段构建配置'); + _buildConfigFromFields(nodeListItem); + return; + } + + late Map json; + try { + json = jsonDecode(nodeListItem.config) as Map; + } catch (e) { + print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段'); + print('📄 Config 内容: ${nodeListItem.config}'); + _buildConfigFromFields(nodeListItem); + return; + } switch (nodeListItem.protocol) { case "vless": final securityConfig = @@ -208,4 +225,95 @@ class KROutboundItem { return {}; } } + + /// 直接从节点字段构建配置(新API格式) + void _buildConfigFromFields(KrNodeListItem nodeListItem) { + switch (nodeListItem.protocol) { + case "shadowsocks": + config = { + "type": "shadowsocks", + "tag": nodeListItem.name, + "server": nodeListItem.serverAddr, + "server_port": nodeListItem.port, + "method": "chacha20-ietf-poly1305", // 默认加密方法 + "password": nodeListItem.uuid + }; + print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}'); + break; + case "vless": + config = { + "type": "vless", + "tag": nodeListItem.name, + "server": nodeListItem.serverAddr, + "server_port": nodeListItem.port, + "uuid": nodeListItem.uuid, + "tls": { + "enabled": true, + "server_name": nodeListItem.serverAddr, + "insecure": false, + "utls": { + "enabled": true, + "fingerprint": "chrome" + } + } + }; + print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}'); + break; + case "vmess": + config = { + "type": "vmess", + "tag": nodeListItem.name, + "server": nodeListItem.serverAddr, + "server_port": nodeListItem.port, + "uuid": nodeListItem.uuid, + "alter_id": 0, + "security": "auto", + "tls": { + "enabled": true, + "server_name": nodeListItem.serverAddr, + "insecure": false, + "utls": {"enabled": true, "fingerprint": "chrome"} + } + }; + print('✅ VMess 节点配置构建成功: ${nodeListItem.name}'); + break; + case "trojan": + config = { + "type": "trojan", + "tag": nodeListItem.name, + "server": nodeListItem.serverAddr, + "server_port": nodeListItem.port, + "password": nodeListItem.uuid, + "tls": { + "enabled": true, + "server_name": nodeListItem.serverAddr, + "insecure": false, + "utls": {"enabled": true, "fingerprint": "chrome"} + } + }; + print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}'); + break; + case "hysteria2": + config = { + "type": "hysteria2", + "tag": nodeListItem.name, + "server": nodeListItem.serverAddr, + "server_port": nodeListItem.port, + "password": nodeListItem.uuid, + "up_mbps": 100, + "down_mbps": 100, + "tls": { + "enabled": true, + "server_name": nodeListItem.serverAddr, + "insecure": false, + "alpn": ["h3"] + } + }; + print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}'); + break; + default: + print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}'); + config = {}; + } + } } diff --git a/lib/app/model/business/kr_outbounds_list.dart b/lib/app/model/business/kr_outbounds_list.dart index ec042a1..ec69cb2 100755 --- a/lib/app/model/business/kr_outbounds_list.dart +++ b/lib/app/model/business/kr_outbounds_list.dart @@ -45,8 +45,15 @@ class KrOutboundsList { } final KROutboundItem item = KROutboundItem(element); + + // 检查节点配置是否有效(必须包含 type 字段) + if (item.config.isEmpty || !item.config.containsKey('type')) { + print('⚠️ 跳过无效节点: ${element.name},配置为空或缺少 type 字段'); + continue; // 跳过无效节点 + } + allList.add(item); - + // 根据标签分组出站项 for (var tag in element.tags) { tagGroups.putIfAbsent(tag, () => []); diff --git a/lib/app/model/response/kr_node_list.dart b/lib/app/model/response/kr_node_list.dart index 3c38d58..a8c6f54 100755 --- a/lib/app/model/response/kr_node_list.dart +++ b/lib/app/model/response/kr_node_list.dart @@ -5,23 +5,39 @@ class KRNodeList { final String subscribeId; final String startTime; final String expireTime; + final bool isTryOut; // 是否是试用订阅 const KRNodeList({ required this.list, this.subscribeId = "0", this.startTime = "", this.expireTime = "", + this.isTryOut = false, }); factory KRNodeList.fromJson(Map json) { - try { - final List? jsonList= json['list'] as List?; + // 新的 API 返回格式: {"list": [{"id": 24, "is_try_out": true, "nodes": [...]}]} + final List? listData = json['list'] as List?; + + if (listData == null || listData.isEmpty) { + KRLogUtil.kr_w('节点列表为空', tag: 'NodeList'); + return const KRNodeList(list: []); + } + + // 获取第一个订阅对象 + final subscribeData = listData[0] as Map; + final bool isTryOut = subscribeData['is_try_out'] as bool? ?? false; + final List? nodesData = subscribeData['nodes'] as List?; + + KRLogUtil.kr_i('节点列表解析: is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', tag: 'NodeList'); + return KRNodeList( - list: jsonList?.map((e) => KrNodeListItem.fromJson(e as Map)).toList() ?? [], - subscribeId: json['id']?.toString() ?? "0", - startTime: json['start_time']?.toString() ?? "", - expireTime: json['expire_time']?.toString() ?? "", + list: nodesData?.map((e) => KrNodeListItem.fromJson(e as Map)).toList() ?? [], + subscribeId: subscribeData['id']?.toString() ?? "0", + startTime: subscribeData['start_time']?.toString() ?? "", + expireTime: subscribeData['expire_time']?.toString() ?? "", + isTryOut: isTryOut, ); } catch (err) { KRLogUtil.kr_e('KRNodeList解析错误: $err', tag: 'NodeList'); @@ -38,6 +54,7 @@ class KrNodeListItem { final String relayMode; final String relayNode; final String serverAddr; + final int port; // 新增:端口字段 final int speedLimit; final List tags; final int traffic; @@ -63,6 +80,7 @@ class KrNodeListItem { this.relayMode = '', this.relayNode = '', required this.serverAddr, + this.port = 0, // 默认值 required this.speedLimit, required this.tags, required this.traffic, @@ -83,6 +101,12 @@ class KrNodeListItem { factory KrNodeListItem.fromJson(Map json) { try { + // 支持新旧两种API格式 + // 新格式: address, port + // 旧格式: server_addr, config 中包含 port + final serverAddr = json['address']?.toString() ?? json['server_addr']?.toString() ?? ''; + final port = _parseIntSafely(json['port']); + return KrNodeListItem( id: _parseIntSafely(json['id']), name: json['name']?.toString() ?? '', @@ -90,7 +114,8 @@ class KrNodeListItem { protocol: json['protocol']?.toString() ?? '', relayMode: json['relay_mode']?.toString() ?? '', relayNode: json['relay_node']?.toString() ?? '', - serverAddr: json['server_addr']?.toString() ?? '', + serverAddr: serverAddr, + port: port, speedLimit: _parseIntSafely(json['speed_limit']), tags: _parseStringList(json['tags']), traffic: _parseIntSafely(json['traffic']), @@ -116,6 +141,7 @@ class KrNodeListItem { uuid: '', protocol: '', serverAddr: '', + port: 0, speedLimit: 0, tags: [], traffic: 0, diff --git a/lib/app/model/response/kr_user_available_subscribe.dart b/lib/app/model/response/kr_user_available_subscribe.dart index 2acf0ee..8958f6f 100755 --- a/lib/app/model/response/kr_user_available_subscribe.dart +++ b/lib/app/model/response/kr_user_available_subscribe.dart @@ -10,6 +10,7 @@ class KRUserAvailableSubscribeItem { final String startTime; final String expireTime; final List list; + final bool isTryOut; // 试用标志:true=试用,false=付费 const KRUserAvailableSubscribeItem({ this.id = 0, @@ -21,19 +22,35 @@ class KRUserAvailableSubscribeItem { this.startTime = '', this.expireTime = '', this.list = const [], + this.isTryOut = false, }); factory KRUserAvailableSubscribeItem.fromJson(Map json) { + // 从 subscribe 对象中获取订阅信息 + final subscribe = json['subscribe'] as Map?; + + // 时间字段可能是 int (毫秒时间戳) 或 String (ISO 8601) + String convertTime(dynamic value) { + if (value == null) return ''; + if (value is String) return value; + if (value is int) { + // 将毫秒时间戳转换为 ISO 8601 字符串 + return DateTime.fromMillisecondsSinceEpoch(value).toIso8601String(); + } + return value.toString(); + } + return KRUserAvailableSubscribeItem( id: json['id'] as int? ?? 0, - name: json['name'] as String? ?? '', - deviceLimit: json['device_limit'] as int? ?? 0, + name: subscribe?['name'] as String? ?? '', + deviceLimit: subscribe?['device_limit'] as int? ?? 0, download: json['download'] as int? ?? 0, upload: json['upload'] as int? ?? 0, traffic: json['traffic'] as int? ?? 0, - startTime: json['start_time'] as String? ?? '', - expireTime: json['expire_time'] as String? ?? '', + startTime: convertTime(json['start_time']), + expireTime: convertTime(json['expire_time']), list: (json['list'] as List?) ?? const [], + isTryOut: subscribe?['is_try_out'] as bool? ?? false, ); } @@ -48,6 +65,7 @@ class KRUserAvailableSubscribeItem { 'start_time': startTime, 'expire_time': expireTime, 'list': list, + 'is_try_out': isTryOut, }; } } diff --git a/lib/app/modules/kr_crisp_chat/controllers/kr_crisp_controller.dart b/lib/app/modules/kr_crisp_chat/controllers/kr_crisp_controller.dart index 674f925..1d280f5 100755 --- a/lib/app/modules/kr_crisp_chat/controllers/kr_crisp_controller.dart +++ b/lib/app/modules/kr_crisp_chat/controllers/kr_crisp_controller.dart @@ -65,7 +65,7 @@ class KRCrispController extends GetxController { try { final appData = KRAppRunData(); final currentLanguage = KRLanguageUtils.getCurrentLanguageCode(); - final userEmail = appData.kr_account ?? ''; + final userEmail = appData.kr_account.value ?? ''; // 获取设备 ID final deviceId = await KRDeviceUtil().kr_getDeviceId(); diff --git a/lib/app/modules/kr_delete_account/controllers/kr_delete_account_controller.dart b/lib/app/modules/kr_delete_account/controllers/kr_delete_account_controller.dart index 64a81d7..69c26cd 100755 --- a/lib/app/modules/kr_delete_account/controllers/kr_delete_account_controller.dart +++ b/lib/app/modules/kr_delete_account/controllers/kr_delete_account_controller.dart @@ -43,23 +43,17 @@ class KRDeleteAccountController extends GetxController { super.onClose(); } - // 发送验证码 + // 发送验证码(仅支持邮箱) Future kr_sendCode() async { - final account = KRAppRunData.getInstance().kr_account; + final account = KRAppRunData.getInstance().kr_account.value; if (account == null || account.isEmpty) { KRCommonUtil.kr_showToast('账号不能为空'); return; } - // 判断账号类型 - final isEmail = KRAppRunData.getInstance().kr_loginType == KRLoginType.kr_email; - final type = isEmail ? KRLoginType.kr_email : KRLoginType.kr_telephone; - - // 发送验证码 + // 发送验证码(简化后的 API 只需要 email 和 type) final result = await _authApi.kr_sendCode( - type, - account, - KRAppRunData.getInstance().kr_areaCode, // 手机号不需要区号 + account, // 邮箱地址 2, // 删除账号的验证码类型 ); @@ -70,7 +64,7 @@ class KRDeleteAccountController extends GetxController { (success) { kr_canSendCode.value = false; kr_countdown.value = 60; - + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { if (kr_countdown.value > 0) { kr_countdown.value--; @@ -89,11 +83,12 @@ class KRDeleteAccountController extends GetxController { KRCommonUtil.kr_showToast(AppTranslations.kr_login.sendCode); return; } - final result = await _authApi.kr_deleteAccount( - KRAppRunData.getInstance().kr_loginType ?? KRLoginType.kr_telephone, + // 删除账号(简化后的 API 只需要 code) + final result = await _authApi.kr_deleteAccount( kr_codeController.text, ); + result.fold( (error) { KRCommonUtil.kr_showToast(error.msg); @@ -103,7 +98,5 @@ class KRDeleteAccountController extends GetxController { KRAppRunData.getInstance().kr_loginOut(); }, ); - // TODO: 实现删除账号的逻辑 - } } diff --git a/lib/app/modules/kr_device_management/bindings/kr_device_management_binding.dart b/lib/app/modules/kr_device_management/bindings/kr_device_management_binding.dart new file mode 100644 index 0000000..28d0aa7 --- /dev/null +++ b/lib/app/modules/kr_device_management/bindings/kr_device_management_binding.dart @@ -0,0 +1,12 @@ +import 'package:get/get.dart'; + +import '../controllers/kr_device_management_controller.dart'; + +class KRDeviceManagementBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => KRDeviceManagementController(), + ); + } +} diff --git a/lib/app/modules/kr_device_management/controllers/kr_device_management_controller.dart b/lib/app/modules/kr_device_management/controllers/kr_device_management_controller.dart new file mode 100644 index 0000000..5266afe --- /dev/null +++ b/lib/app/modules/kr_device_management/controllers/kr_device_management_controller.dart @@ -0,0 +1,294 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; +import 'package:kaer_with_panels/app/utils/kr_device_util.dart'; +import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart'; +import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart'; +import 'package:kaer_with_panels/app/common/app_run_data.dart'; +import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart'; +import 'package:kaer_with_panels/app/services/kr_site_config_service.dart'; +import 'package:kaer_with_panels/app/services/kr_device_info_service.dart'; +import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart'; +import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart'; +import 'dart:io'; +import 'dart:math'; + +class KRDeviceManagementController extends GetxController { + // 设备列表 + final RxList> devices = >[].obs; + + // 加载状态 + final RxBool isLoading = true.obs; + + // 当前设备ID + String? currentDeviceId; + + @override + void onInit() { + super.onInit(); + _initDeviceId(); + loadDeviceList(); + } + + /// 初始化当前设备ID + Future _initDeviceId() async { + try { + currentDeviceId = await KRDeviceUtil().kr_getDeviceId(); + KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement'); + } catch (e) { + KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement'); + } + } + + /// 加载设备列表 + Future loadDeviceList() async { + try { + isLoading.value = true; + KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement'); + + // 调用API获取设备列表 + final result = await KRUserApi().kr_getUserDevices(); + + result.fold( + (error) { + KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement'); + Get.snackbar('错误', error.msg); + }, + (deviceList) { + KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement'); + + // 转换设备数据格式 + devices.value = deviceList.map((device) { + final identifier = device['identifier']?.toString() ?? ''; + final isCurrent = identifier == currentDeviceId; + + return { + 'id': device['id']?.toString() ?? '', + 'identifier': identifier, + 'device_name': device['user_agent'] ?? '未知设备', + 'ip': device['ip'] ?? '', + 'last_login': device['updated_at'] ?? device['created_at'] ?? '', + 'is_current': isCurrent, + 'enabled': device['enabled'] ?? true, + 'online': device['online'] ?? false, + }; + }).toList(); + }, + ); + } catch (e, stackTrace) { + KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement'); + KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement'); + Get.snackbar('错误', '加载设备列表失败'); + } finally { + isLoading.value = false; + } + } + + /// 删除设备 + Future deleteDevice(String id) async { + try { + // 检查是否是本机设备 + final device = devices.firstWhere( + (d) => d['id'] == id, + orElse: () => {}, + ); + + if (device.isEmpty) return; + + final isCurrent = device['is_current'] ?? false; + + // 使用响应式变量来接收确认结果 + bool? confirmed; + + // 显示确认对话框 + await KRDialog.show( + title: '确认删除', + message: isCurrent + ? '确定要删除本机设备吗?删除后将使用设备登录自动重新登录。' + : '确定要删除此设备吗?删除后该设备将被强制下线。', + icon: Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: Colors.red.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.warning_rounded, + color: Colors.red, + size: 32, + ), + ), + confirmText: '删除', + cancelText: '取消', + onConfirm: () { + confirmed = true; + }, + onCancel: () { + confirmed = false; + }, + ); + + if (confirmed != true) return; + + KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement'); + + // 调用API解绑设备 + final result = await KRUserApi().kr_unbindUserDevice(id); + + result.fold( + (error) { + KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement'); + Get.snackbar('错误', '删除失败:${error.msg}'); + }, + (_) async { + KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement'); + + if (isCurrent) { + // 如果删除的是本机设备,重新进行设备登录 + KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement'); + + // 先关闭当前设备管理页面 + Get.back(); + + // 执行重新登录 + await _reloginWithDevice(); + } else { + // 删除其他设备,从列表中移除 + devices.removeWhere((device) => device['id'] == id); + Get.snackbar('成功', '设备已删除'); + } + }, + ); + } catch (e, stackTrace) { + KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement'); + KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement'); + Get.snackbar('错误', '删除失败:$e'); + } + } + + /// 重新使用设备登录 + Future _reloginWithDevice() async { + try { + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement'); + KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement'); + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement'); + + // 先清除当前的用户信息(但不调用 kr_loginOut,避免显示登录界面) + final appRunData = KRAppRunData.getInstance(); + appRunData.kr_isLogin.value = false; + appRunData.kr_token = null; + appRunData.kr_account.value = null; + appRunData.kr_userId.value = null; + + // 检查是否启用设备登录 + final siteConfigService = KRSiteConfigService(); + final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled(); + + if (!isDeviceLoginEnabled) { + KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement'); + Get.snackbar('提示', '设备登录未启用,请手动登录'); + await appRunData.kr_loginOut(); + return; + } + + KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement'); + + // 初始化设备信息服务(如果还没初始化) + await KRDeviceInfoService().initialize(); + + // 调用设备登录接口 + final authApi = KRAuthApi(); + final result = await authApi.kr_deviceLogin(); + + result.fold( + (error) { + // 设备登录失败 + KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement'); + Get.snackbar('错误', '自动登录失败:${error.msg},请手动登录'); + + // 执行完整退出登录,显示登录界面 + appRunData.kr_loginOut(); + }, + (token) async { + // 设备登录成功 + KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement'); + KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement'); + + // 保存新的用户信息 + final deviceId = KRDeviceInfoService().deviceId ?? 'unknown'; + await appRunData.kr_saveUserInfo( + token, + 'device_$deviceId', + KRLoginType.kr_email, + null, + ); + + KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement'); + + // 等待一小段时间,确保登录状态已经更新 + await Future.delayed(const Duration(milliseconds: 300)); + + // 刷新订阅信息 + KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement'); + try { + await KRSubscribeService().kr_refreshAll(); + KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement'); + } catch (e) { + KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement'); + } + + Get.snackbar('成功', '已自动重新登录'); + }, + ); + } catch (e, stackTrace) { + KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement'); + KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement'); + Get.snackbar('错误', '自动登录失败,请手动登录'); + + // 发生异常,执行完整退出登录 + await KRAppRunData.getInstance().kr_loginOut(); + } + } + + /// 获取设备类型和图标 + Map getDeviceTypeInfo(String userAgent) { + String deviceType = '未知设备'; + String iconName = 'devices'; + + if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) { + deviceType = '安卓设备'; + iconName = 'phone_android'; + } else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) { + deviceType = 'iOS 设备'; + iconName = 'phone_iphone'; + } else if (userAgent.contains('iPad')) { + deviceType = 'iPad'; + iconName = 'tablet'; + } else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) { + deviceType = 'macOS'; + iconName = 'desktop_mac'; + } else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) { + deviceType = 'Windows'; + iconName = 'computer'; + } else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) { + deviceType = 'Linux'; + iconName = 'computer'; + } + + return { + 'type': deviceType, + 'icon': iconName, + }; + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } +} diff --git a/lib/app/modules/kr_device_management/views/kr_device_management_view.dart b/lib/app/modules/kr_device_management/views/kr_device_management_view.dart new file mode 100644 index 0000000..a432804 --- /dev/null +++ b/lib/app/modules/kr_device_management/views/kr_device_management_view.dart @@ -0,0 +1,313 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart'; + +import '../controllers/kr_device_management_controller.dart'; + +class KRDeviceManagementView extends GetView { + const KRDeviceManagementView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + backgroundColor: Theme.of(context).primaryColor, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color.fromRGBO(23, 151, 255, 0.15), + Color.fromRGBO(23, 151, 255, 0.05), + ], + stops: [0.0, 0.28], + ), + ), + child: Column( + children: [ + // 顶部导航栏 + AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: Theme.of(context).iconTheme.color, + size: 20.w, + ), + onPressed: () => Get.back(), + ), + title: Text( + '设备管理', + style: KrAppTextStyle( + color: Theme.of(context).textTheme.bodyMedium?.color, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + // 内容区域 + Expanded( + child: Obx(() { + if (controller.isLoading.value) { + return Center( + child: CircularProgressIndicator(), + ); + } + + if (controller.devices.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.devices_other, + size: 64.w, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + SizedBox(height: 16.w), + Text( + '暂无登录设备', + style: KrAppTextStyle( + fontSize: 14, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + ); + } + + return RefreshIndicator( + onRefresh: () => controller.loadDeviceList(), + child: ListView.builder( + padding: EdgeInsets.all(16.w), + itemCount: controller.devices.length, + itemBuilder: (context, index) { + return _buildDeviceItem( + context, + controller.devices[index], + ); + }, + ), + ); + }), + ), + ], + ), + ), + ); + } + + /// 构建设备项 + Widget _buildDeviceItem( + BuildContext context, Map device) { + final id = device['id'] ?? ''; + final identifier = device['identifier'] ?? ''; + final userAgent = device['device_name'] ?? '未知设备'; + final isCurrent = device['is_current'] ?? false; + final ip = device['ip'] ?? ''; + final lastLoginRaw = device['last_login']; + final String lastLogin = lastLoginRaw?.toString() ?? ''; + + // 获取设备类型信息 + final deviceInfo = controller.getDeviceTypeInfo(userAgent); + final deviceType = deviceInfo['type'] as String; + final iconName = deviceInfo['icon'] as String; + + return Container( + margin: EdgeInsets.only(bottom: 12.w), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.03), + blurRadius: 10.w, + offset: Offset(0, 2.w), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 设备类型和操作按钮 + Row( + children: [ + // 设备图标 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: const Color(0xFF1797FF).withOpacity(0.1), + borderRadius: BorderRadius.circular(8.w), + ), + child: Icon( + _getIconData(iconName), + color: const Color(0xFF1797FF), + size: 24.w, + ), + ), + SizedBox(width: 12.w), + // 设备信息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + deviceType, + style: KrAppTextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + if (isCurrent) ...[ + SizedBox(width: 8.w), + Container( + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 2.w, + ), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + '本机', + style: KrAppTextStyle( + fontSize: 10, + color: Colors.green, + ), + ), + ), + ], + ], + ), + SizedBox(height: 4.w), + Text( + 'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...', + style: KrAppTextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ], + ), + ), + // 删除按钮 + TextButton( + onPressed: () => controller.deleteDevice(id), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.error, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), + ), + child: Text( + '删除', + style: KrAppTextStyle( + fontSize: 14, + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ], + ), + // 分隔线 + if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[ + SizedBox(height: 12.w), + Divider(height: 1, color: Theme.of(context).dividerColor), + SizedBox(height: 12.w), + ], + // 详细信息 + if (ip.isNotEmpty) + _buildInfoRow( + context, + 'IP地址', + ip, + ), + if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.w), + if (lastLogin.isNotEmpty) + _buildInfoRow( + context, + '最后登录', + _formatDateTime(lastLoginRaw), + ), + ], + ), + ); + } + + /// 构建信息行 + Widget _buildInfoRow(BuildContext context, String label, String value) { + return Row( + children: [ + Text( + '$label: ', + style: KrAppTextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + Expanded( + child: Text( + value, + style: KrAppTextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } + + /// 格式化时间 + String _formatDateTime(dynamic timestamp) { + if (timestamp == null) return '未知'; + + try { + DateTime dateTime; + if (timestamp is int) { + // 判断是秒级时间戳(10位)还是毫秒级时间戳(13位) + if (timestamp > 9999999999) { + // 毫秒级时间戳 + dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); + } else { + // 秒级时间戳 + dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); + } + } else if (timestamp is String) { + dateTime = DateTime.parse(timestamp); + } else { + return '未知'; + } + return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'; + } catch (e) { + return '未知'; + } + } + + /// 获取图标数据 + IconData _getIconData(String iconName) { + switch (iconName) { + case 'phone_android': + return Icons.phone_android; + case 'phone_iphone': + return Icons.phone_iphone; + case 'tablet': + return Icons.tablet_mac; + case 'desktop_mac': + return Icons.desktop_mac; + case 'computer': + return Icons.computer; + default: + return Icons.devices; + } + } +} diff --git a/lib/app/modules/kr_home/controllers/kr_home_controller.dart b/lib/app/modules/kr_home/controllers/kr_home_controller.dart index 57780f9..69239f1 100755 --- a/lib/app/modules/kr_home/controllers/kr_home_controller.dart +++ b/lib/app/modules/kr_home/controllers/kr_home_controller.dart @@ -183,10 +183,7 @@ class KRHomeController extends GetxController { if (isValidLogin) { kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn; KRLogUtil.kr_i('设置为已登录状态', tag: 'HomeController'); - - // 检查公告服务 - KRAnnouncementService().kr_checkAnnouncement(); - + // 确保订阅服务初始化 _kr_ensureSubscribeServiceInitialized(); } else { @@ -255,7 +252,15 @@ class KRHomeController extends GetxController { } else { kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn; KRLogUtil.kr_i('登录状态变化:设置为未登录', tag: 'HomeController'); + + // 重置列表状态,防止出现无限高度 + kr_currentListStatus.value = KRHomeViewsListStatus.kr_none; + + // 退出登录清理订阅服务 kr_subscribeService.kr_logout(); + + // 显式更新底部面板高度,确保未登录状态下高度正确 + kr_updateBottomPanelHeight(); } } catch (e) { KRLogUtil.kr_e('处理登录状态变化失败: $e', tag: 'HomeController'); diff --git a/lib/app/modules/kr_home/views/kr_home_connection_options_view.dart b/lib/app/modules/kr_home/views/kr_home_connection_options_view.dart index 311abaf..c36d82a 100755 --- a/lib/app/modules/kr_home/views/kr_home_connection_options_view.dart +++ b/lib/app/modules/kr_home/views/kr_home_connection_options_view.dart @@ -26,30 +26,13 @@ class KRHomeConnectionOptionsView extends GetView { ), ), SizedBox(height: 8.w), - Row( - children: [ - Flexible( - child: _buildConnectionOption( - "home_server", - AppTranslations.kr_home.dedicatedServers, - context, - onTap: () { - controller.kr_switchListStatus(KRHomeViewsListStatus.kr_serverList); - }, - ), - ), - SizedBox(width: 12.w), - Flexible( - child: _buildConnectionOption( - "home_ct", - AppTranslations.kr_home.countryRegion, - context, - onTap: () { - controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList); - }, - ), - ), - ], + _buildConnectionOption( + "home_ct", + AppTranslations.kr_home.countryRegion, + context, + onTap: () { + controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList); + }, ), ], ); diff --git a/lib/app/modules/kr_home/views/kr_home_view.dart b/lib/app/modules/kr_home/views/kr_home_view.dart index 3c8a4ae..f7d69b4 100755 --- a/lib/app/modules/kr_home/views/kr_home_view.dart +++ b/lib/app/modules/kr_home/views/kr_home_view.dart @@ -31,28 +31,32 @@ class KRHomeView extends GetView { // 地图视图 const KRHomeMapView(), // 登录视图 - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - spreadRadius: 0, - blurRadius: 10, - offset: const Offset(0, -2), - ), - ], + Align( + alignment: Alignment.bottomCenter, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + child: Container( + width: double.infinity, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 0, + blurRadius: 10, + offset: const Offset(0, -2), + ), + ], + ), + child: const KRLoginView(), ), - child: const KRLoginView(), ), ), ], diff --git a/lib/app/modules/kr_invite/controllers/kr_invite_controller.dart b/lib/app/modules/kr_invite/controllers/kr_invite_controller.dart index bf6b75f..63a1a91 100755 --- a/lib/app/modules/kr_invite/controllers/kr_invite_controller.dart +++ b/lib/app/modules/kr_invite/controllers/kr_invite_controller.dart @@ -71,16 +71,15 @@ class KRInviteController extends GetxController { } } + // ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口 + // 邀请码现在使用 AppConfig 中的固定值,等待新接口实现 Future _kr_fetchUserInfo() async { try { kr_isLoading.value = true; - final either = await KRUserApi().kr_getUserInfo(); - either.fold( - (error) => KRCommonUtil.kr_showToast(error.msg), - (userInfo) { - kr_referCode.value = userInfo.referCode; - }, - ); + + // 使用 AppConfig 中的固定邀请码 + kr_referCode.value = AppConfig.kr_userReferCode; + } catch (e) { KRCommonUtil.kr_showToast(e.toString()); } finally { diff --git a/lib/app/modules/kr_login/controllers/kr_login_controller.dart b/lib/app/modules/kr_login/controllers/kr_login_controller.dart index 8b82ce9..9d86e88 100755 --- a/lib/app/modules/kr_login/controllers/kr_login_controller.dart +++ b/lib/app/modules/kr_login/controllers/kr_login_controller.dart @@ -9,6 +9,7 @@ import 'package:kaer_with_panels/app/model/kr_area_code.dart'; import 'package:kaer_with_panels/app/utils/kr_common_util.dart'; import 'package:kaer_with_panels/app/localization/app_translations.dart'; import 'package:kaer_with_panels/app/utils/kr_event_bus.dart'; +import 'package:kaer_with_panels/app/services/kr_site_config_service.dart'; import '../../../localization/kr_language_utils.dart'; @@ -125,9 +126,9 @@ class KRLoginController extends GetxController String kr_getNextBtnText() { switch (kr_loginStatus.value) { case KRLoginProgressStatus.kr_check: - return AppTranslations.kr_login.next; + return AppTranslations.kr_login.passwordLogin; // 显示"登录" case KRLoginProgressStatus.kr_loginByCode: - return AppTranslations.kr_login.codeLogin; + return AppTranslations.kr_login.passwordLogin; // 已废弃,保留以防兼容性问题 case KRLoginProgressStatus.kr_loginByPsd: return AppTranslations.kr_login.passwordLogin; case KRLoginProgressStatus.kr_registerSendCode: @@ -198,25 +199,17 @@ class KRLoginController extends GetxController } }); - // 修改 accountController 的监听器 + // accountController 监听器(仅支持邮箱) accountController.addListener(() { String input = accountController.text.trim(); - // 延迟执行状态更新,避免在输入过程中频繁切换 + // 延迟执行状态更新 Future.microtask(() { - final isNumeric = _isNumeric(input); - if (isNumeric && kr_loginType.value != KRLoginType.kr_telephone) { - kr_loginType.value = KRLoginType.kr_telephone; - kr_emailList.clear(); - kr_removeOverlay(); - } else if (!isNumeric && kr_loginType.value != KRLoginType.kr_email) { - kr_loginType.value = KRLoginType.kr_email; - } + // 始终保持邮箱类型 + kr_loginType.value = KRLoginType.kr_email; - // 只在邮箱模式下更新邮箱列表 - if (!isNumeric) { - kr_emailList.value = kr_generateAndSortEmailList(input); - } + // 更新邮箱建议列表 + kr_emailList.value = kr_generateAndSortEmailList(input); kr_accountHasText.value = input.isNotEmpty; }); @@ -259,40 +252,29 @@ class KRLoginController extends GetxController return numericRegex.hasMatch(input); } - /// 检查是否注册 + /// 直接登录(不再检查是否注册) void kr_check() async { if (accountController.text.isEmpty) { KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount); return; } - final either = await KRAuthApi().kr_isRegister( - kr_loginType.value, - accountController.text, - kr_loginType == KRLoginType.kr_telephone - ? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode - : null); - either.fold((l) { - KRCommonUtil.kr_showToast(l.msg); - }, (r) async { - kr_isRegistered.value = r; - kr_loginStatus.value = r - ? KRLoginProgressStatus.kr_loginByPsd - : KRLoginProgressStatus.kr_registerSendCode; - }); + if (psdController.text.isEmpty) { + KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword); + return; + } + + // 直接调用登录 + kr_login(); } - /// 发送验证码 + /// 发送验证码(仅支持邮箱) void kr_sendCode() async { final either = await KRAuthApi().kr_sendCode( - kr_loginType.value, accountController.text, - kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode, kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode - ? 1 - : kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSendCode - ? 2 - : 2); + ? 2 // 注册验证码类型为2 + : 3); // 重置密码验证码类型为3 either.fold((l) { KRCommonUtil.kr_showToast(l.msg); }, (r) async { @@ -301,28 +283,15 @@ class KRLoginController extends GetxController }); } - /// 开始登录 + /// 开始登录(仅支持邮箱+密码) void kr_login() async { - if (kr_loginStatus == KRLoginProgressStatus.kr_loginByCode && - codeController.text.isEmpty) { - KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode); - return; - } - - if (kr_loginStatus == KRLoginProgressStatus.kr_loginByPsd && - psdController.text.isEmpty) { + if (psdController.text.isEmpty) { KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword); return; } final either = await KRAuthApi().kr_login( - kr_loginType.value, - kr_loginStatus.value == KRLoginProgressStatus.kr_loginByPsd, accountController.text, - kr_loginType == KRLoginType.kr_telephone - ? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode - : null, - codeController.text, psdController.text); either.fold((l) { KRCommonUtil.kr_showToast(l.msg); @@ -331,8 +300,14 @@ class KRLoginController extends GetxController }); } - /// 开始注册 + /// 开始注册(仅支持邮箱,验证码和邀请码可选) void kr_register() async { + // 验证邮箱 + if (accountController.text.isEmpty) { + KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount); + return; + } + if (psdController.text.isEmpty) { KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword); return; @@ -346,20 +321,26 @@ class KRLoginController extends GetxController return; } + // 检查是否需要验证码(基于站点配置) + final siteConfig = KRSiteConfigService(); + final needVerification = siteConfig.isEmailVerificationEnabled() || + siteConfig.isRegisterVerificationEnabled(); + + if (needVerification && codeController.text.isEmpty) { + KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode); + return; + } + final either = await KRAuthApi().kr_register( - kr_loginType.value, accountController.text, - kr_loginType == KRLoginType.kr_telephone - ? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode - : null, - codeController.text, psdController.text, - inviteCode: inviteCodeController.text); + code: codeController.text.isEmpty ? null : codeController.text, + inviteCode: inviteCodeController.text.isEmpty ? null : inviteCodeController.text); either.fold((l) { KRCommonUtil.kr_showToast(l.msg); }, (r) async { _saveLoginData(r); - KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess); + KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess); }); } @@ -390,20 +371,17 @@ class KRLoginController extends GetxController } } - /// 验证验证码 + /// 验证验证码(仅支持邮箱) void kr_checkVerificationCode(KRLoginProgressStatus status) async { final either = await KRAuthApi().kr_checkVerificationCode( - kr_loginType.value, accountController.text, - kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode, codeController.text, kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode - ? 1 - : 2); + ? 2 // 注册验证码类型为2 + : 3); // 重置密码验证码类型为3 either.fold((l) { KRCommonUtil.kr_showToast(l.msg); }, (r) async { - if (status == KRLoginProgressStatus.kr_registerSendCode) { kr_loginStatus.value = KRLoginProgressStatus.kr_registerSetPsd; } else if (status == KRLoginProgressStatus.kr_forgetPsdSendCode) { @@ -412,7 +390,7 @@ class KRLoginController extends GetxController }); } - /// 忘记密码--- 设置新密码 + /// 忘记密码-设置新密码(仅支持邮箱) void kr_setNewPsdByForgetPsd() async { if (psdController.text.isEmpty) { KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword); @@ -428,11 +406,7 @@ class KRLoginController extends GetxController } final either = await KRAuthApi().kr_setNewPsdByForgetPsd( - kr_loginType.value, accountController.text, - kr_loginType == KRLoginType.kr_telephone - ? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode - : null, codeController.text, psdController.text); either.fold((l) { @@ -464,15 +438,13 @@ class KRLoginController extends GetxController }); } - /// 设置登录数据 + /// 设置登录数据(仅支持邮箱) void _saveLoginData(String token) { KRAppRunData.getInstance().kr_saveUserInfo( token, accountController.text, - kr_loginType.value, - kr_loginType == KRLoginType.kr_telephone - ? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode - : null); + KRLoginType.kr_email, + null); kr_loginStatus.value = KRLoginProgressStatus.kr_check; // 登录/注册成功后,发送消息触发订阅服务刷新 @@ -480,6 +452,9 @@ class KRLoginController extends GetxController Future.delayed(Duration(milliseconds: 100), () { KREventBus().kr_sendMessage(KRMessageType.kr_payment); }); + + // 登录成功后返回到上一页 + Get.back(); } /// 根据输入内容匹配邮箱 @@ -611,6 +586,14 @@ class KRLoginController extends GetxController // } } + @override + void onReady() { + super.onReady(); + // 每次打开登录页面时,重置为登录状态(而不是注册状态) + // 这样确保点击"登录/注册"按钮时始终显示登录页面 + kr_loginStatus.value = KRLoginProgressStatus.kr_check; + } + @override void onClose() { kr_removeOverlay(); @@ -640,13 +623,9 @@ class KRLoginController extends GetxController void _updateInputState() { String input = accountController.text.trim(); - if (_isNumeric(input)) { - kr_loginType.value = KRLoginType.kr_telephone; - kr_emailList.clear(); - } else { - kr_loginType.value = KRLoginType.kr_email; - kr_emailList.value = kr_generateAndSortEmailList(input); - } + // 始终保持邮箱类型 + kr_loginType.value = KRLoginType.kr_email; + kr_emailList.value = kr_generateAndSortEmailList(input); } // 添加移除悬浮框的方法 diff --git a/lib/app/modules/kr_login/views/kr_login_view.dart b/lib/app/modules/kr_login/views/kr_login_view.dart index bd99ab0..9c9e639 100755 --- a/lib/app/modules/kr_login/views/kr_login_view.dart +++ b/lib/app/modules/kr_login/views/kr_login_view.dart @@ -14,6 +14,7 @@ import '../controllers/kr_login_controller.dart'; import 'package:kaer_with_panels/app/localization/app_translations.dart'; import 'package:kaer_with_panels/app/services/api_service/api.dart'; import 'package:kaer_with_panels/app/common/app_config.dart'; +import 'package:kaer_with_panels/app/services/kr_site_config_service.dart'; class KRLoginView extends GetView { const KRLoginView({super.key}); @@ -21,15 +22,16 @@ class KRLoginView extends GetView { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return GestureDetector( - onTap: () { - if (!FocusScope.of(context).hasPrimaryFocus) { - FocusScope.of(context).unfocus(); - } - _hideDropdown(); - }, - child: Container( - color: theme.scaffoldBackgroundColor, + return Scaffold( + backgroundColor: theme.scaffoldBackgroundColor, + body: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + if (!FocusScope.of(context).hasPrimaryFocus) { + FocusScope.of(context).unfocus(); + } + _hideDropdown(); + }, child: SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 20.w), child: Obx(() { @@ -58,16 +60,18 @@ class KRLoginView extends GetView { } Widget _buildCheckView(BuildContext context) { - // 构建检查视图的代码 + // 构建登录视图 - 直接显示邮箱和密码输入框 return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + // 总是显示返回按钮,包括 kr_check 状态 + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), + // 邮箱输入框 _buildInputSection(context, Theme.of(context)), + SizedBox(height: 8.w), + // 密码输入框 + _buildPasswordInput(Theme.of(context)), _buildDynamicContent(Theme.of(context)), _buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)), SizedBox(height: _getBottomPadding()), @@ -75,6 +79,63 @@ class KRLoginView extends GetView { ); } + /// 构建密码输入框(专用于 kr_check 状态) + Widget _buildPasswordInput(ThemeData theme) { + return Container( + width: double.infinity, + height: 52.w, + decoration: ShapeDecoration( + color: theme.cardColor, + shape: RoundedRectangleBorder( + side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)), + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.w), + child: _buildIcon("login_psd"), + ), + SizedBox(width: 8.w), + Expanded( + child: Obx(() => TextField( + controller: controller.psdController, + obscureText: controller.kr_obscureText.value, + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: '请输入密码', + hintStyle: theme.textTheme.bodySmall?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16.w), + suffixIcon: controller.kr_psdHasText.value + ? IconButton( + icon: Icon( + controller.kr_obscureText.value + ? Icons.visibility_off + : Icons.visibility, + color: const Color(0xFF999999), + ), + onPressed: () { + controller.kr_obscureText.value = !controller.kr_obscureText.value; + }, + ) + : null, + ), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + )), + ), + ], + ), + ); + } + /// 获取底部间距 double _getBottomPadding() { switch (controller.kr_loginStatus.value) { @@ -98,10 +159,7 @@ class KRLoginView extends GetView { return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), _buildInputSection(context, Theme.of(context)), _buildDynamicContent(Theme.of(context)), @@ -116,10 +174,7 @@ class KRLoginView extends GetView { return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), _buildInputSection(context, Theme.of(context)), _buildDynamicContent(Theme.of(context)), @@ -130,18 +185,29 @@ class KRLoginView extends GetView { } Widget _buildRegisterSendCodeView(BuildContext context) { - // 构建注册发送验证码视图的代码 + // 构建单页注册视图 - 所有字段显示在一个页面 + final theme = Theme.of(context); return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), - _buildHeaderSection(Theme.of(context)), - _buildInputSection(context, Theme.of(context)), - _buildDynamicContent(Theme.of(context)), - _buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)), + _buildBackButton(theme), + _buildHeaderSection(theme), + // 邮箱输入框 + _buildRegistrationEmailInput(theme), + SizedBox(height: 8.w), + // 验证码输入框(根据站点配置决定是否显示,包括底部间距) + _buildRegistrationCodeInputWithSpacing(theme), + // 密码输入框 + _buildRegistrationPasswordInput(theme), + SizedBox(height: 8.w), + // 确认密码输入框 + _buildRegistrationConfirmPasswordInput(theme), + SizedBox(height: 8.w), + // 邀请码输入框(可选) + _buildInviteCodeInput(theme), + SizedBox(height: 17.w), + // 注册按钮 + _buildNextButton('注册账号', theme), SizedBox(height: _getBottomPadding()), ], ); @@ -152,10 +218,7 @@ class KRLoginView extends GetView { return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), _buildInputSection(context, Theme.of(context)), Column( @@ -178,10 +241,7 @@ class KRLoginView extends GetView { return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), _buildInputSection(context, Theme.of(context)), _buildDynamicContent(Theme.of(context)), @@ -196,10 +256,7 @@ class KRLoginView extends GetView { return Column( mainAxisSize: MainAxisSize.min, children: [ - Obx(() => Visibility( - visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check, - child: _buildBackButton(Theme.of(context)), - )), + _buildBackButton(Theme.of(context)), _buildHeaderSection(Theme.of(context)), _buildInputSection(context, Theme.of(context)), if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSetPsd) @@ -670,7 +727,8 @@ class KRLoginView extends GetView { Widget? content; switch (controller.kr_loginStatus.value) { case KRLoginProgressStatus.kr_check: - content = _buildAgreementText(theme); + // 在登录状态显示"忘记密码"和切换按钮 + content = _buildLoginBtns(theme); case KRLoginProgressStatus.kr_loginByCode: case KRLoginProgressStatus.kr_loginByPsd: content = _buildLoginBtns(theme); @@ -744,6 +802,34 @@ class KRLoginView extends GetView { /// 构建登录按钮行 Widget _buildLoginBtns(ThemeData theme) { + // 在 kr_check 状态下显示不同的按钮 + if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) { + return SizedBox( + height: 24.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _buildHoverableTextButton( + text: 'login.forgotPassword'.tr, + onTap: () => controller.kr_loginStatus.value = + KRLoginProgressStatus.kr_forgetPsdSendCode, + theme: theme, + ), + SizedBox(width: 16.w), + _buildHoverableTextButton( + text: '点我注册', + onTap: () { + // 跳转到注册页面 + controller.kr_loginStatus.value = KRLoginProgressStatus.kr_registerSendCode; + }, + theme: theme, + ), + ], + ), + ); + } + + // 其他状态保持原有逻辑 return SizedBox( height: 24.w, child: Row( @@ -802,7 +888,8 @@ class KRLoginView extends GetView { controller.kr_login(); break; case KRLoginProgressStatus.kr_registerSendCode: - controller.kr_checkCode(); + // 直接调用注册,不再需要多步骤 + controller.kr_register(); break; case KRLoginProgressStatus.kr_registerSetPsd: controller.kr_register(); @@ -981,8 +1068,21 @@ class KRLoginView extends GetView { Widget _buildBackButton(ThemeData theme) { return GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { - controller.kr_back(); + print('👆 返回按钮被点击了!'); + print('👆 当前登录状态: ${controller.kr_loginStatus.value}'); + // 只有在初始登录状态(kr_check)时,返回按钮才返回主页 + // 其他所有状态(包括注册页面)都返回到上一步 + if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) { + print('👆 调用 Get.back() 返回主页'); + Get.back(); + print('👆 Get.back() 调用完成'); + } else { + // 其他状态:返回到上一步(例如从注册页返回登录页) + print('👆 调用 controller.kr_back() 返回上一步'); + controller.kr_back(); + } }, child: Padding( padding: EdgeInsets.fromLTRB(0, 20.w, 0, 0), @@ -1095,4 +1195,293 @@ class KRLoginView extends GetView { ), ); } + + /// 构建可悬停的文本按钮 + Widget _buildHoverableTextButton({ + required String text, + required VoidCallback onTap, + required ThemeData theme, + }) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: StatefulBuilder( + builder: (context, setState) { + bool isHovering = false; + return MouseRegion( + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Text( + text, + style: theme.textTheme.bodySmall?.copyWith( + fontSize: 13.sp, + color: isHovering ? const Color(0xFF1796FF) : const Color(0xFF666666), + fontWeight: FontWeight.w500, + fontFamily: 'AlibabaPuHuiTi-Medium', + ), + ), + ), + ); + }, + ), + ); + } + + /// ========== 注册页面相关组件 ========== + + /// 构建注册页面的邮箱输入框 + Widget _buildRegistrationEmailInput(ThemeData theme) { + return Container( + width: double.infinity, + height: 52.w, + decoration: ShapeDecoration( + color: theme.cardColor, + shape: RoundedRectangleBorder( + side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)), + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.w), + child: _buildIcon("login_account"), + ), + SizedBox(width: 8.w), + Expanded( + child: TextField( + controller: controller.accountController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'login.enterEmail'.tr, + hintStyle: theme.textTheme.bodySmall?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16.w), + ), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + ), + ), + Obx(() => Visibility( + visible: controller.kr_accountHasText.value, + child: GestureDetector( + onTap: () => controller.accountController.clear(), + child: Container( + height: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: _buildIcon("login_close"), + ), + ), + )), + ], + ), + ); + } + + /// 构建注册页面的验证码输入框(包含间距) + Widget _buildRegistrationCodeInputWithSpacing(ThemeData theme) { + // 从站点配置服务获取验证配置(站点配置不是响应式的,不需要 Obx) + final siteConfig = KRSiteConfigService(); + final needVerification = siteConfig.isEmailVerificationEnabled() || + siteConfig.isRegisterVerificationEnabled(); + + // 如果不需要验证码,返回空容器 + if (!needVerification) { + return SizedBox.shrink(); + } + + // 显示验证码输入框和底部间距 + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildRegistrationCodeInput(theme), + SizedBox(height: 8.w), + ], + ); + } + + /// 构建注册页面的验证码输入框(根据站点配置显示) + Widget _buildRegistrationCodeInput(ThemeData theme) { + // 从站点配置服务获取验证配置 + final siteConfig = KRSiteConfigService(); + final needVerification = siteConfig.isEmailVerificationEnabled() || + siteConfig.isRegisterVerificationEnabled(); + + // 如果不需要验证码,返回空容器 + if (!needVerification) { + return SizedBox.shrink(); + } + + // 显示验证码输入框 + return Container( + width: double.infinity, + height: 52.w, + decoration: ShapeDecoration( + color: theme.cardColor, + shape: RoundedRectangleBorder( + side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)), + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.w), + child: _buildIcon("login_code"), + ), + SizedBox(width: 8.w), + Expanded( + child: TextField( + controller: controller.codeController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'login.enterCode'.tr, + hintStyle: theme.textTheme.bodySmall?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16.w), + ), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + ), + ), + if (controller.kr_codeHasText.value) + GestureDetector( + onTap: () => controller.codeController.clear(), + child: Container( + height: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 8.w), + child: _buildIcon("login_close"), + ), + ), + _buildSendCodeButton(theme), + ], + ), + ); + } + + /// 构建注册页面的密码输入框 + Widget _buildRegistrationPasswordInput(ThemeData theme) { + return Container( + width: double.infinity, + height: 52.w, + decoration: ShapeDecoration( + color: theme.cardColor, + shape: RoundedRectangleBorder( + side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)), + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.w), + child: _buildIcon("login_psd"), + ), + SizedBox(width: 8.w), + Expanded( + child: Obx(() => TextField( + controller: controller.psdController, + obscureText: controller.kr_obscureText.value, + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: 'login.enterPassword'.tr, + hintStyle: theme.textTheme.bodySmall?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16.w), + suffixIcon: controller.kr_psdHasText.value + ? IconButton( + icon: Icon( + controller.kr_obscureText.value + ? Icons.visibility_off + : Icons.visibility, + color: const Color(0xFF999999), + ), + onPressed: () { + controller.kr_obscureText.value = !controller.kr_obscureText.value; + }, + ) + : null, + ), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + )), + ), + ], + ), + ); + } + + /// 构建注册页面的确认密码输入框 + Widget _buildRegistrationConfirmPasswordInput(ThemeData theme) { + return Container( + width: double.infinity, + height: 52.w, + decoration: ShapeDecoration( + color: theme.cardColor, + shape: RoundedRectangleBorder( + side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)), + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12.w), + child: _buildIcon("login_psd"), + ), + SizedBox(width: 8.w), + Expanded( + child: Obx(() => TextField( + controller: controller.agPsdController, + obscureText: controller.kr_obscureText.value, + keyboardType: TextInputType.text, + decoration: InputDecoration( + hintText: 'login.reenterPassword'.tr, + hintStyle: theme.textTheme.bodySmall?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 16.w), + suffixIcon: controller.kr_agPsdHasText.value + ? IconButton( + icon: Icon( + controller.kr_obscureText.value + ? Icons.visibility_off + : Icons.visibility, + color: const Color(0xFF999999), + ), + onPressed: () { + controller.kr_obscureText.value = !controller.kr_obscureText.value; + }, + ) + : null, + ), + style: theme.textTheme.bodyMedium?.copyWith( + fontSize: 14.sp, + fontFamily: 'AlibabaPuHuiTi-Regular', + ), + )), + ), + ], + ), + ); + } } diff --git a/lib/app/modules/kr_purchase_membership/controllers/kr_purchase_membership_controller.dart b/lib/app/modules/kr_purchase_membership/controllers/kr_purchase_membership_controller.dart index 0175f86..5c3697c 100755 --- a/lib/app/modules/kr_purchase_membership/controllers/kr_purchase_membership_controller.dart +++ b/lib/app/modules/kr_purchase_membership/controllers/kr_purchase_membership_controller.dart @@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart'; import 'package:kaer_with_panels/app/utils/kr_log_util.dart'; import '../../../common/app_run_data.dart'; +import '../../../common/app_config.dart'; import '../../../model/response/kr_already_subscribe.dart'; import '../../../model/response/kr_payment_methods.dart'; import '../../../routes/app_pages.dart'; @@ -109,15 +110,20 @@ class KRPurchaseMembershipController extends GetxController { /// 初始化用户信息 Future _iniUserInfo() async { - final either0 = await KRUserApi().kr_getUserInfo(); - either0.fold( - (error) { - KRLogUtil.kr_e(error.msg, tag: 'AppRunData'); - }, - (userInfo) async { - _kr_balance = userInfo.balance; - }, - ); + // ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口 + // 余额现在使用 AppConfig 中的固定值,等待新接口实现 + // final either0 = await KRUserApi().kr_getUserInfo(); + // either0.fold( + // (error) { + // KRLogUtil.kr_e(error.msg, tag: 'AppRunData'); + // }, + // (userInfo) async { + // _kr_balance = userInfo.balance; + // }, + // ); + + // 使用 AppConfig 中的固定余额 + _kr_balance = AppConfig.kr_userBalance; } /// 获取用户已订阅套餐 diff --git a/lib/app/modules/kr_setting/views/kr_setting_view.dart b/lib/app/modules/kr_setting/views/kr_setting_view.dart index ae7d63e..613d195 100755 --- a/lib/app/modules/kr_setting/views/kr_setting_view.dart +++ b/lib/app/modules/kr_setting/views/kr_setting_view.dart @@ -8,6 +8,7 @@ import '../controllers/kr_setting_controller.dart'; import '../../../themes/kr_theme_service.dart'; import '../../../localization/app_translations.dart'; import '../../../routes/app_pages.dart'; +import '../../../common/app_run_data.dart'; class KRSettingView extends GetView { const KRSettingView({Key? key}) : super(key: key); @@ -235,12 +236,33 @@ class KRSettingView extends GetView { onChanged: (value) => controller.kr_helpImprove.value = value, ), _kr_buildDivider(), - _kr_buildActionTile( - context, - title: AppTranslations.kr_userInfo.myAccount, - trailing: AppTranslations.kr_setting.goToDelete, - onTap: controller.kr_deleteAccount, - ), + Obx(() { + final appRunData = KRAppRunData.getInstance(); + final isLoggedIn = appRunData.kr_isLogin.value; + final isDeviceLogin = appRunData.isDeviceLogin(); + + if (!isLoggedIn) { + // 未登录,不显示此项 + return SizedBox.shrink(); + } else if (isDeviceLogin) { + // 设备登录(游客模式),显示"点击这里登录/注册" + return _kr_buildActionTile( + context, + title: "登录/注册", + trailing: "", + onTap: () => Get.toNamed(Routes.MR_LOGIN), + ); + } else { + // 正常登录,显示用户邮箱 + final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount; + return _kr_buildActionTile( + context, + title: userEmail, + trailing: AppTranslations.kr_setting.goToDelete, + onTap: controller.kr_deleteAccount, + ); + } + }), _kr_buildDivider(), // _kr_buildTitleTile( // context, @@ -407,7 +429,7 @@ class KRSettingView extends GetView { style: KrAppTextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodyMedium?.color, + color: Theme.of(context).textTheme.bodySmall?.color, ), ), trailing: Text( diff --git a/lib/app/modules/kr_splash/controllers/kr_splash_controller.dart b/lib/app/modules/kr_splash/controllers/kr_splash_controller.dart index 3252b42..a4f57ed 100755 --- a/lib/app/modules/kr_splash/controllers/kr_splash_controller.dart +++ b/lib/app/modules/kr_splash/controllers/kr_splash_controller.dart @@ -241,13 +241,29 @@ class KRSplashController extends GetxController { } // 等待一小段时间确保所有初始化完成 - await Future.delayed(const Duration(milliseconds: 200)); + await Future.delayed(const Duration(milliseconds: 500)); // 验证登录状态是否已正确设置 final loginStatus = KRAppRunData.getInstance().kr_isLogin.value; - KRLogUtil.kr_i('启动完成,最终登录状态: $loginStatus', tag: 'SplashController'); + final token = KRAppRunData.getInstance().kr_token; + final hasToken = token != null && token.isNotEmpty; - // 直接导航到主页 + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + print('🎯 准备进入主页'); + print('📊 最终登录状态: $loginStatus'); + print('🎫 Token存在: $hasToken'); + if (hasToken) { + print('🎫 Token前缀: ${token.substring(0, min(20, token.length))}...'); + } + print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController'); + KRLogUtil.kr_i('🎯 准备进入主页', tag: 'SplashController'); + KRLogUtil.kr_i('📊 最终登录状态: $loginStatus', tag: 'SplashController'); + KRLogUtil.kr_i('🎫 Token存在: $hasToken', tag: 'SplashController'); + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController'); + + // 直接导航到主页(无论是否登录,主页会根据登录状态显示不同内容) Get.offAllNamed(Routes.KR_MAIN); } catch (e) { // 后续步骤失败,显示错误信息 diff --git a/lib/app/modules/kr_statistics/controllers/kr_statistics_controller.dart b/lib/app/modules/kr_statistics/controllers/kr_statistics_controller.dart index 91dc121..d2adc3e 100755 --- a/lib/app/modules/kr_statistics/controllers/kr_statistics_controller.dart +++ b/lib/app/modules/kr_statistics/controllers/kr_statistics_controller.dart @@ -102,13 +102,15 @@ class KRStatisticsController extends GetxController { return; } - final either0 = await KRUserApi().kr_getUserInfo(); - either0.fold( - (error) => KRCommonUtil.kr_showToast(error.msg), - (userInfo) { - // kr_homeController.kr_userId.value = userInfo.id.toString(); - }, - ); + // ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口 + // 用户ID 现在从其他途径获取,此处注释掉 + // final either0 = await KRUserApi().kr_getUserInfo(); + // either0.fold( + // (error) => KRCommonUtil.kr_showToast(error.msg), + // (userInfo) { + // // kr_homeController.kr_userId.value = userInfo.id.toString(); + // }, + // ); // 获取本周的开始和结束时间戳 final DateTime now = DateTime.now(); diff --git a/lib/app/modules/kr_user_info/controllers/kr_user_info_controller.dart b/lib/app/modules/kr_user_info/controllers/kr_user_info_controller.dart index e5f68d5..8194707 100755 --- a/lib/app/modules/kr_user_info/controllers/kr_user_info_controller.dart +++ b/lib/app/modules/kr_user_info/controllers/kr_user_info_controller.dart @@ -199,15 +199,21 @@ class KRUserInfoController extends GetxController with KRAppBarOpacityMixin { if (!KRAppRunData.getInstance().kr_isLogin.value) { return; } - final either0 = await KRUserApi().kr_getUserInfo(); - either0.fold( - (error) { - KRLogUtil.kr_e(error.msg, tag: 'AppRunData'); - }, - (userInfo) async { - kr_balance.value = userInfo.balance.toDouble() / 100; - }, - ); + + // ⚠️ 已废弃:新版本后端不再提供 kr_getUserInfo 接口 + // 余额现在使用 AppConfig 中的固定值,等待新接口实现 + // final either0 = await KRUserApi().kr_getUserInfo(); + // either0.fold( + // (error) { + // KRLogUtil.kr_e(error.msg, tag: 'AppRunData'); + // }, + // (userInfo) async { + // kr_balance.value = userInfo.balance.toDouble() / 100; + // }, + // ); + + // 使用 AppConfig 中的固定余额 + kr_balance.value = AppConfig.kr_userBalance.toDouble() / 100; } /// 处理用户退出登录 diff --git a/lib/app/modules/kr_user_info/views/kr_user_info_view.dart b/lib/app/modules/kr_user_info/views/kr_user_info_view.dart index 0a8f6fb..8303fb1 100755 --- a/lib/app/modules/kr_user_info/views/kr_user_info_view.dart +++ b/lib/app/modules/kr_user_info/views/kr_user_info_view.dart @@ -99,64 +99,115 @@ class KRUserInfoView extends GetView { // 构建绑定提示 Widget _kr_buildBindingTip(BuildContext context) { - return Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( + return Obx(() { + final appRunData = KRAppRunData.getInstance(); + final isLoggedIn = appRunData.kr_isLogin.value; + final isDeviceLogin = appRunData.isDeviceLogin(); + + // 判断显示文字和颜色 + String displayText; + Color displayColor; + IconData displayIcon; + bool shouldShowLoginPrompt = false; + + if (!isLoggedIn) { + // 未登录 + displayText = AppTranslations.kr_userInfo.bindingTip; + displayColor = Theme.of(context).colorScheme.error; + displayIcon = Icons.info_outline; + shouldShowLoginPrompt = false; + } else if (isDeviceLogin) { + // 设备登录(游客模式) + displayText = "登录/注册"; + displayColor = const Color(0xFF1797FF); // 使用蓝色提示可以点击 + displayIcon = Icons.touch_app; + shouldShowLoginPrompt = true; + } else { + // 正常登录 + displayText = "${AppTranslations.kr_userInfo.myAccount} ${appRunData.kr_account.value}"; + displayColor = Theme.of(context).textTheme.bodyMedium?.color ?? Colors.black; + displayIcon = Icons.info; + shouldShowLoginPrompt = false; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: shouldShowLoginPrompt + ? () { + // 跳转到登录页面 + Get.toNamed(Routes.MR_LOGIN); + } + : null, + child: Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w), + decoration: shouldShowLoginPrompt + ? BoxDecoration( + color: const Color(0xFF1797FF).withOpacity(0.05), + borderRadius: BorderRadius.circular(8.w), + ) + : null, child: Row( children: [ Icon( - KRAppRunData.getInstance().kr_isLogin.value - ? Icons.info - : Icons.info_outline, - color: !KRAppRunData.getInstance().kr_isLogin.value - ? Theme.of(context).colorScheme.error - : Theme.of(context).textTheme.bodyMedium?.color, + displayIcon, + color: displayColor, + size: 16.w, + ), + SizedBox(width: 8.w), + Expanded( + child: Text( + displayText, + style: KrAppTextStyle( + color: displayColor, + fontSize: 12, + fontWeight: shouldShowLoginPrompt ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + if (shouldShowLoginPrompt) + Icon( + Icons.arrow_forward_ios, + size: 14.w, + color: displayColor, + ), + ], + ), + ), + ), + // 余额信息或游客ID + Visibility( + visible: KRAppRunData.getInstance().kr_isLogin.value && + AppConfig.getInstance().kr_is_daytime, + child: Padding( + padding: EdgeInsets.only(left: 16.w, bottom: 16.w), + child: Row( + children: [ + Icon( + appRunData.isDeviceLogin() + ? Icons.person_outline + : Icons.account_balance_wallet_outlined, + color: Theme.of(context).textTheme.bodyMedium?.color, size: 16.w, ), SizedBox(width: 8.w), Text( - KRAppRunData.getInstance().kr_isLogin.value - ? "${AppTranslations.kr_userInfo.myAccount} ${KRAppRunData().kr_account}" - : AppTranslations.kr_userInfo.bindingTip, + appRunData.isDeviceLogin() + ? "游客ID:${(appRunData.kr_userId.value ?? 0) + 10000}" + : "${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}", style: KrAppTextStyle( - color: !KRAppRunData.getInstance().kr_isLogin.value - ? Theme.of(context).colorScheme.error - : Theme.of(context).textTheme.bodyMedium?.color, fontSize: 12, + color: Theme.of(context).textTheme.bodyMedium?.color, ), ), ], ), ), - // 余额信息(写死预览) - Visibility( - visible: KRAppRunData.getInstance().kr_isLogin.value && - AppConfig.getInstance().kr_is_daytime, - child: Padding( - padding: EdgeInsets.only(left: 16.w, bottom: 16.w), - child: Row( - children: [ - Icon( - Icons.account_balance_wallet_outlined, - color: Theme.of(context).textTheme.bodyMedium?.color, - size: 16.w, - ), - SizedBox(width: 8.w), - Text( - "${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}", - style: KrAppTextStyle( - fontSize: 12, - color: Theme.of(context).textTheme.bodyMedium?.color, - ), - ), - ], - ), - ), - ), - ], - )); + ), + ], + ); + }); } // 构建订阅卡片 @@ -574,49 +625,67 @@ class KRUserInfoView extends GetView { // 构建快捷键区域 Widget _kr_buildShortcutSection(BuildContext context) { - return Container( - margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppTranslations.kr_userInfo.shortcuts, - style: KrAppTextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).textTheme.bodyMedium?.color, + return Obx(() { + final appRunData = KRAppRunData.getInstance(); + final isLoggedIn = appRunData.kr_isLogin.value; + final isDeviceLogin = appRunData.isDeviceLogin(); + + // 只有正常登录用户(非游客)才显示设备管理 + final showDeviceManagement = isLoggedIn && !isDeviceLogin; + + return Container( + margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppTranslations.kr_userInfo.shortcuts, + style: KrAppTextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), ), - ), - SizedBox(height: 12.w), - Column( - children: [ - _kr_buildShortcutContainer( - icon: "my_ads", - title: AppTranslations.kr_userInfo.adBlock, - value: controller.kr_isAdBlockEnabled, - onChanged: controller.kr_toggleAdBlock, - context: context, - ), - _kr_buildShortcutContainer( - icon: "my_dns", - title: AppTranslations.kr_userInfo.ndsUnlock, - value: controller.kr_isNDSUnlockEnabled, - onChanged: controller.kr_toggleNDSUnlock, - context: context, - ), - _kr_buildShortcutContainer( - icon: "my_cn_us", - title: AppTranslations.kr_userInfo.contactUs, - onTap: () { - Get.toNamed(Routes.KR_CRISP); - }, - context: context, - ), - ], - ), - ], - ), - ); + SizedBox(height: 12.w), + Column( + children: [ + _kr_buildShortcutContainer( + icon: "my_ads", + title: AppTranslations.kr_userInfo.adBlock, + value: controller.kr_isAdBlockEnabled, + onChanged: controller.kr_toggleAdBlock, + context: context, + ), + _kr_buildShortcutContainer( + icon: "my_dns", + title: AppTranslations.kr_userInfo.ndsUnlock, + value: controller.kr_isNDSUnlockEnabled, + onChanged: controller.kr_toggleNDSUnlock, + context: context, + ), + if (showDeviceManagement) + _kr_buildShortcutContainer( + icon: "my_dns", + title: "设备管理", + onTap: () { + Get.toNamed(Routes.KR_DEVICE_MANAGEMENT); + }, + context: context, + ), + _kr_buildShortcutContainer( + icon: "my_cn_us", + title: AppTranslations.kr_userInfo.contactUs, + onTap: () { + Get.toNamed(Routes.KR_CRISP); + }, + context: context, + ), + ], + ), + ], + ), + ); + }); } // 构建快捷键容器 diff --git a/lib/app/network/base_response.dart b/lib/app/network/base_response.dart index 05cd007..94907e1 100755 --- a/lib/app/network/base_response.dart +++ b/lib/app/network/base_response.dart @@ -34,6 +34,10 @@ class BaseResponse { final decrypted = KRAesUtil.decryptData(cipherText, nonce, AppConfig.kr_encryptionKey); body = jsonDecode(decrypted); KRLogUtil.kr_i('✅ 解密成功', tag: 'BaseResponse'); + + // 打印完整的解密后数据,方便调试 + final bodyStr = jsonEncode(body); + KRLogUtil.kr_i('📦 解密后数据(完整): $bodyStr', tag: 'BaseResponse'); } catch (e) { KRLogUtil.kr_e('❌ 解密失败: $e,使用原始数据', tag: 'BaseResponse'); body = dataMap; diff --git a/lib/app/network/http_util.dart b/lib/app/network/http_util.dart index 45240ed..5c3e535 100755 --- a/lib/app/network/http_util.dart +++ b/lib/app/network/http_util.dart @@ -25,7 +25,7 @@ import '../utils/kr_log_util.dart'; // import 'package:video/app/utils/log_util.dart'; /// 定义请求方法的枚举 -enum HttpMethod { GET, POST, DELETE } +enum HttpMethod { GET, POST, DELETE, PUT } /// 封装请求 class HttpUtil { @@ -167,6 +167,15 @@ class HttpUtil { headers: headers, // 添加请求头 ), ); + } else if (method == HttpMethod.PUT) { + responseTemp = await _dio.put>( + path, + data: map, + options: Options( + contentType: "application/json", + headers: headers, // 添加请求头 + ), + ); } else { responseTemp = await _dio.post>( path, diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 3421d83..d665eac 100755 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -32,6 +32,8 @@ import '../modules/kr_order_status/bindings/kr_order_status_binding.dart'; import '../modules/kr_order_status/views/kr_order_status_view.dart'; import '../modules/kr_splash/bindings/kr_splash_binding.dart'; import '../modules/kr_splash/views/kr_splash_view.dart'; +import '../modules/kr_device_management/bindings/kr_device_management_binding.dart'; +import '../modules/kr_device_management/views/kr_device_management_view.dart'; part 'app_routes.dart'; @@ -121,5 +123,10 @@ class AppPages { page: () => const KRCrispView(), binding: KRCrispBinding(), ), + GetPage( + name: _Paths.KR_DEVICE_MANAGEMENT, + page: () => const KRDeviceManagementView(), + binding: KRDeviceManagementBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index e494ead..ed599c5 100755 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -20,6 +20,7 @@ abstract class Routes { static const KR_WEBVIEW = _Paths.KR_WEBVIEW; static const KR_ORDER_STATUS = '/kr-order-status'; static const KR_CRISP = _Paths.KR_CRISP; + static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT; } abstract class _Paths { @@ -40,4 +41,5 @@ abstract class _Paths { static const KR_DELETE_ACCOUNT = '/kr-delete-account'; static const KR_WEBVIEW = '/kr_webview'; static const KR_CRISP = '/kr-crisp'; + static const KR_DEVICE_MANAGEMENT = '/kr-device-management'; } diff --git a/lib/app/services/api_service/api.dart b/lib/app/services/api_service/api.dart index 8e350c0..a026c68 100755 --- a/lib/app/services/api_service/api.dart +++ b/lib/app/services/api_service/api.dart @@ -3,21 +3,17 @@ abstract class Api { /// 游客登录查看是否已经注册 static const String kr_isRegister = "/v1/app/auth/check"; - /// 注册1024 - static const String kr_register = "/v1/app/auth/register"; + /// 注册 + static const String kr_register = "/v1/auth/register"; /// 验证验证码 - static const String kr_checkVerificationCode = - "/v1/common/check_verification_code"; + static const String kr_checkVerificationCode = "/v1/auth/check-code"; - /// 发送手机验证码 - static const String kr_sendPhoneCode = "/v1/common/send_sms_code"; - - /// 发送邮箱验证码 - static const String kr_sendEmailCode = "/v1/common/send_code"; + /// 发送验证码(统一接口,支持邮箱和手机) + static const String kr_sendCode = "/v1/auth/send-code"; /// 登录接口 - static const String kr_login = "/v1/app/auth/login"; + static const String kr_login = "/v1/auth/login"; /// 设备登录(游客登录) /// 参考 OmnOem 项目 ppanel.json 配置 @@ -29,8 +25,8 @@ abstract class Api { /// 忘记密码-设置新密码 static const String kr_setNewPsdByForgetPsd = "/v1/app/auth/reset_password"; - /// 节点信息 - static const String kr_nodeList = "/v1/app/node/list"; + /// 节点信息(包含试用/付费标志) + static const String kr_nodeList = "/v1/public/subscribe/node/list"; /// 获取用户订阅流量日志 static const String kr_nodeGroupList = "/v1/app/node/rule_group_list"; @@ -48,15 +44,15 @@ abstract class Api { static const String kr_checkout = "/v1/app/order/checkout"; /// 获取可购买套餐 - static const String kr_getPackageList = "/v1/app/subscribe/list"; + static const String kr_getPackageList = "/v1/public/subscribe/list"; - /// 获取用户已订阅套餐 + /// 获取用户已订阅套餐(用于判断是否购买过) static const String kr_getAlreadySubscribe = - "/v1/app/subscribe/user/already_subscribe"; + "/v1/public/user/subscribe"; - /// 获取用户可用订阅 + /// 获取用户可用订阅(与已订阅接口相同,OmnOem 项目中没有区分) static const String kr_userAvailableSubscribe = - "/v1/app/subscribe/user/available_subscribe"; + "/v1/public/user/subscribe"; /// 续费 static const String kr_renewal = "/v1/app/order/renewal"; @@ -66,7 +62,7 @@ abstract class Api { static const String kr_orderDetail = "/v1/app/order/detail"; /// 获取消息列表 - static const String kr_getMessageList = "/v1/app/announcement/list"; + static const String kr_getMessageList = "/v1/public/announcement/list"; /// 获取邀请数据 // static const String kr_getInviteData = "/v1/public/invite/code"; @@ -74,9 +70,6 @@ abstract class Api { /// 配置信息 static const String kr_config = "/v1/app/auth/config"; - /// 获取用户信息 - static const String kr_getUserInfo = "/v1/app/user/info"; - /// 获取用户在线时长统计 static const String kr_getUserOnlineTimeStatistics = "/v1/app/user/online_time/statistics"; @@ -96,4 +89,10 @@ abstract class Api { /// 重置订阅周期 static const String kr_resetSubscribePeriod = "/v1/app/subscribe/reset/period"; + + /// 获取用户设备列表 + static const String kr_getUserDevices = "/v1/public/user/devices"; + + /// 解绑用户设备 + static const String kr_unbindUserDevice = "/v1/public/user/unbind_device"; } diff --git a/lib/app/services/api_service/kr_api.user.dart b/lib/app/services/api_service/kr_api.user.dart index befd291..cffd47a 100755 --- a/lib/app/services/api_service/kr_api.user.dart +++ b/lib/app/services/api_service/kr_api.user.dart @@ -83,21 +83,22 @@ class KRUserApi { return right(baseResponse.model); } - Future> kr_getUserInfo() async { - final Map data = {}; - BaseResponse baseResponse = - await HttpUtil.getInstance().request( - Api.kr_getUserInfo, - data, - method: HttpMethod.GET, - isShowLoading: false, - ); - if (!baseResponse.isSuccess) { - return left( - HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); - } - return right(baseResponse.model); - } + // ⚠️ 已废弃:新版本后端不再提供此接口 + // Future> kr_getUserInfo() async { + // final Map data = {}; + // BaseResponse baseResponse = + // await HttpUtil.getInstance().request( + // Api.kr_getUserInfo, + // data, + // method: HttpMethod.GET, + // isShowLoading: false, + // ); + // if (!baseResponse.isSuccess) { + // return left( + // HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); + // } + // return right(baseResponse.model); + // } Future> kr_getAffiliateCount() async { final Map data = {}; @@ -132,4 +133,64 @@ class KRUserApi { return right(baseResponse.model); } + /// 获取用户设备列表 + Future>>> kr_getUserDevices() async { + final Map data = {}; + + BaseResponse baseResponse = + await HttpUtil.getInstance().request( + Api.kr_getUserDevices, + data, + method: HttpMethod.GET, + isShowLoading: false, + ); + + if (!baseResponse.isSuccess) { + return left( + HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); + } + + // 返回设备列表数据 + try { + // 响应格式: { data: { list: [...], total: N } } + final responseData = baseResponse.model; + final List> devices = + (responseData['list'] as List) + .map((item) => item as Map) + .toList(); + return right(devices); + } catch (e) { + KRLogUtil.kr_e('解析设备列表失败: $e', tag: 'KRUserApi'); + return left(HttpError(msg: '数据解析失败', code: -1)); + } + } + + /// 解绑用户设备 + Future> kr_unbindUserDevice(String deviceId) async { + final Map data = {}; + + // 将字符串 ID 转换为整数 + try { + data['id'] = int.parse(deviceId); + } catch (e) { + KRLogUtil.kr_e('设备ID格式错误: $deviceId', tag: 'KRUserApi'); + return left(HttpError(msg: '设备ID格式错误', code: -1)); + } + + BaseResponse baseResponse = + await HttpUtil.getInstance().request( + Api.kr_unbindUserDevice, + data, + method: HttpMethod.PUT, + isShowLoading: true, + ); + + if (!baseResponse.isSuccess) { + return left( + HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); + } + + return right(null); + } + } diff --git a/lib/app/services/api_service/kr_auth_api.dart b/lib/app/services/api_service/kr_auth_api.dart index 2e102d2..4be31c7 100755 --- a/lib/app/services/api_service/kr_auth_api.dart +++ b/lib/app/services/api_service/kr_auth_api.dart @@ -23,22 +23,14 @@ import '../../common/app_config.dart'; import 'package:dio/dio.dart' as dio; class KRAuthApi { - /// 是否开启了审核开关 - Future> kr_isRegister( - KRLoginType tpye, String account, String? areaCode) async { + /// 检查账号是否已注册(仅支持邮箱) + Future> kr_isRegister(String email) async { final Map data = {}; - data['method'] = tpye.value; - data['account'] = account; - - final deviceId = await KRDeviceUtil().kr_getDeviceId(); - KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi'); - data["identifier"] = deviceId; + data['email'] = email; - data["user_agent"] = _kr_getUserAgent(); - data["os"] = _kr_getUserAgent(); - if (areaCode != null) { - data['area_code'] = areaCode.toString(); - } + final deviceId = await KRDeviceUtil().kr_getDeviceId(); + KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi'); + data["identifier"] = deviceId; BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_isRegister, data, @@ -52,29 +44,26 @@ class KRAuthApi { return right(baseResponse.model.kr_isRegister); } - /// 注册 + /// 注册(仅支持邮箱+密码,验证码和邀请码可选) Future> kr_register( - KRLoginType tpye, - String account, - String? areaCode, - String? code, - String? password, - {String? inviteCode}) async { + String email, + String password, + {String? code, + String? inviteCode}) async { final Map data = {}; - data['method'] = tpye.value; - data['account'] = account; + data['email'] = email; data['password'] = password; - data["code"] = code; data["identifier"] = await KRDeviceUtil().kr_getDeviceId(); - data["os"] = _kr_getUserAgent(); + // 验证码是可选的,只有在提供时才发送 + if (code != null && code.isNotEmpty) { + data["code"] = code; + } + + // 邀请码是可选的 if (inviteCode != null && inviteCode.isNotEmpty) { data["invite"] = inviteCode; } - data["user_agent"] = _kr_getUserAgent(); - if (tpye == KRLoginType.kr_telephone) { - data['area_code'] = areaCode; - } BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_register, data, @@ -87,20 +76,14 @@ class KRAuthApi { return right(baseResponse.model.kr_token.toString()); } - /// 验证验证码 - Future> kr_checkVerificationCode( KRLoginType tpye, - String account, String? areaCode, String code, int type) async { - + /// 验证验证码(仅支持邮箱) + Future> kr_checkVerificationCode( + String email, String code, int type) async { final Map data = {}; - data['method'] = tpye.value; - if(tpye == KRLoginType.kr_telephone){ - data['account'] = areaCode.toString() + account; - - }else{ - data['account'] = account; - } + data['email'] = email; data['code'] = code; data['type'] = type; + BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_checkVerificationCode, data, method: HttpMethod.POST, isShowLoading: true); @@ -108,37 +91,23 @@ class KRAuthApi { return left( HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); } - if(baseResponse.model.kr_isRegister){ + if (baseResponse.model.kr_isRegister) { return right(true); - }else{ + } else { return left(HttpError(msg: "error.70001".tr, code: 70001)); } - - } - /// 登陆 - Future> kr_login(KRLoginType tpye, bool isPsd, - String account, String? areaCode, String? code, String? password) async { + /// 登录(仅支持邮箱+密码) + Future> kr_login( + String email, String password) async { final Map data = {}; - data['method'] = tpye.value; - data['account'] = account; + data['email'] = email; + data['password'] = password; - - final deviceId = await KRDeviceUtil().kr_getDeviceId(); + final deviceId = await KRDeviceUtil().kr_getDeviceId(); KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi'); - data["identifier"] = deviceId; - data["user_agent"] = _kr_getUserAgent(); - data["os"] = _kr_getUserAgent(); - if (tpye == KRLoginType.kr_telephone) { - data['area_code'] = areaCode; - } - - if (isPsd) { - data['password'] = password; - } else { - data["code"] = code; - } + data["identifier"] = deviceId; BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_login, data, @@ -151,45 +120,30 @@ class KRAuthApi { return right(baseResponse.model.kr_token.toString()); } - /// 发送验证码 type 1 注册 其他 2 - Future> kr_sendCode( - KRLoginType tpye, String account, String? areaCode, int type) async { + /// 发送验证码(仅支持邮箱) + /// type: 1=登录, 2=注册, 3=重置密码 + Future> kr_sendCode(String email, int type) async { final Map data = {}; - - if (tpye == KRLoginType.kr_email) { - data['email'] = account; - } else { - data['telephone'] = account; - data['telephone_area_code'] = areaCode.toString(); - } + data['email'] = email; data['type'] = type; + BaseResponse baseResponse = await HttpUtil.getInstance() - .request( - tpye == KRLoginType.kr_email - ? Api.kr_sendEmailCode - : Api.kr_sendPhoneCode, - data, - method: HttpMethod.POST, - isShowLoading: true); + .request(Api.kr_sendCode, data, + method: HttpMethod.POST, isShowLoading: true); if (!baseResponse.isSuccess) { return left( HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode)); } - // KRCommonUtil.kr_showToast(baseResponse.model.toString()); - // KRIsRegister model = (baseResponse..model) as KRIsRegister; return right(true); } /// 删除账号 - Future> kr_deleteAccount(KRLoginType tpye, - String code) async { + Future> kr_deleteAccount(String code) async { final Map data = {}; - data['method'] = tpye.value; - data['code'] = code; - - BaseResponse baseResponse = await HttpUtil.getInstance() + + BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_deleteAccount, data, method: HttpMethod.DELETE, isShowLoading: true); if (!baseResponse.isSuccess) { @@ -198,23 +152,16 @@ class KRAuthApi { } return right(""); - } - /// 忘记密码-设置新密码 - Future> kr_setNewPsdByForgetPsd(KRLoginType tpye, - String account, String? areaCode, String? code, String? password) async { + /// 忘记密码-设置新密码(仅支持邮箱) + Future> kr_setNewPsdByForgetPsd( + String email, String code, String password) async { final Map data = {}; - data['method'] = tpye.value; - data['account'] = account; + data['email'] = email; data['password'] = password; data["code"] = code; - data["identifier"] = await KRDeviceUtil().kr_getDeviceId(); - data["user_agent"] = _kr_getUserAgent(); - data["os"] = _kr_getUserAgent(); - if (tpye == KRLoginType.kr_telephone) { - data['area_code'] = areaCode; - } + data["identifier"] = await KRDeviceUtil().kr_getDeviceId(); BaseResponse baseResponse = await HttpUtil.getInstance() .request(Api.kr_setNewPsdByForgetPsd, data, diff --git a/lib/app/services/kr_announcement_service.dart b/lib/app/services/kr_announcement_service.dart index 7812e04..cc09ef2 100755 --- a/lib/app/services/kr_announcement_service.dart +++ b/lib/app/services/kr_announcement_service.dart @@ -21,6 +21,11 @@ class KRAnnouncementService { KRAnnouncementService._internal(); + // 重置公告显示状态(用于退出登录时) + void kr_reset() { + _kr_hasShownAnnouncement = false; + } + // 检查是否需要显示公告弹窗 Future kr_checkAnnouncement() async { if (_kr_hasShownAnnouncement) { diff --git a/lib/app/services/kr_subscribe_service.dart b/lib/app/services/kr_subscribe_service.dart index 93fae60..57f24e8 100755 --- a/lib/app/services/kr_subscribe_service.dart +++ b/lib/app/services/kr_subscribe_service.dart @@ -72,6 +72,9 @@ class KRSubscribeService { /// 是否处于试用状态 final RxBool kr_isTrial = false.obs; + /// 当前节点列表是否包含试用节点 + final RxBool kr_hasTrialNodes = false.obs; + /// 订阅记录 final RxList kr_alreadySubscribe = [].obs; @@ -247,9 +250,13 @@ class KRSubscribeService { result.fold((error) { kr_currentStatus.value = KRSubscribeServiceStatus.kr_error; }, (nodes) { - // 处理节点列表 + // 记录当前节点列表是否包含试用节点 + kr_hasTrialNodes.value = nodes.isTryOut; + KRLogUtil.kr_i('切换订阅 - 节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService'); + + // 处理节点列表(不使用分组) final listModel = KrOutboundsList(); - listModel.processOutboundItems(nodes.list, kr_nodeGroups); + listModel.processOutboundItems(nodes.list, []); // 更新UI数据 groupOutboundList.value = listModel.groupOutboundList; @@ -277,24 +284,45 @@ class KRSubscribeService { _kr_trialTimer?.cancel(); _kr_subscriptionTimer?.cancel(); - // 检查试用状态 - final bool kr_isSubscribed = kr_currentSubscribe.value != null && - kr_alreadySubscribe.any((subscribe) => - kr_currentSubscribe.value?.id == subscribe.userSubscribeId); + if (kr_currentSubscribe.value == null) { + kr_isTrial.value = false; + return; + } - KRLogUtil.kr_i('当前订阅状态: ${kr_isSubscribed ? "已订阅" : "未订阅"}', - tag: 'SubscribeService'); - KRLogUtil.kr_i('当前订阅ID: ${kr_currentSubscribe.value?.id}', - tag: 'SubscribeService'); - KRLogUtil.kr_i( - '已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', - tag: 'SubscribeService'); + // 优先使用 API 返回的 isTryOut 字段判断试用状态 + final currentSubscribe = kr_currentSubscribe.value!; - // 设置试用状态 - kr_isTrial.value = kr_currentSubscribe.value != null && !kr_isSubscribed; + // 1. 优先使用 API 返回的 isTryOut 字段 + kr_isTrial.value = currentSubscribe.isTryOut; + KRLogUtil.kr_i('步骤1 - API isTryOut 字段: ${currentSubscribe.isTryOut}', tag: 'SubscribeService'); - KRLogUtil.kr_i('试用状态: ${kr_isTrial.value ? "是" : "否"}', - tag: 'SubscribeService'); + // 2. 如果 API 说不是试用,检查是否有购买记录 + if (!kr_isTrial.value) { + final bool kr_isSubscribed = kr_alreadySubscribe.any( + (subscribe) => currentSubscribe.id == subscribe.userSubscribeId + ); + KRLogUtil.kr_i('步骤2 - 检查购买记录: $kr_isSubscribed', tag: 'SubscribeService'); + + // 如果没有购买记录,判断为试用 + if (!kr_isSubscribed) { + kr_isTrial.value = true; + KRLogUtil.kr_i('步骤2 - 没有购买记录,判定为试用', tag: 'SubscribeService'); + } + } + + // 3. 最后检查订阅名称是否包含"试用"关键字(最后的备用方案) + if (!kr_isTrial.value && currentSubscribe.name.contains('试用')) { + kr_isTrial.value = true; + KRLogUtil.kr_i('步骤3 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService'); + } + + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService'); + KRLogUtil.kr_i('当前订阅: ${currentSubscribe.name}(${currentSubscribe.id})', tag: 'SubscribeService'); + KRLogUtil.kr_i('API isTryOut: ${currentSubscribe.isTryOut}', tag: 'SubscribeService'); + KRLogUtil.kr_i('已订阅记录数: ${kr_alreadySubscribe.length}', tag: 'SubscribeService'); + KRLogUtil.kr_i('已订阅ID列表: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', tag: 'SubscribeService'); + KRLogUtil.kr_i('✅ 最终试用状态: ${kr_isTrial.value ? "试用" : "付费"}', tag: 'SubscribeService'); + KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService'); if (kr_isTrial.value) { // 启动试用倒计时 @@ -445,15 +473,10 @@ class KRSubscribeService { }, ); - final result = await kr_subscribeApi.kr_nodeGroupList(); - result.fold( - (error) { - throw Exception('获取节点分组失败: ${error.msg}'); - }, - (groups) { - kr_nodeGroups.value = groups; - }, - ); + // 🔧 取消节点分组的概念,不再调用 kr_nodeGroupList + // 直接使用节点列表,通过 is_try_out 字段区分免费/付费节点 + kr_nodeGroups.clear(); + KRLogUtil.kr_i('已取消节点分组,将直接使用节点列表', tag: 'SubscribeService'); // 保存当前选中的订阅名称 final currentSubscribeID = kr_currentSubscribe.value?.id; @@ -502,9 +525,26 @@ class KRSubscribeService { } } - // 2. 如果没有找到之前的订阅,优先选择已购买的套餐(非试用) + // 2. 如果没有找到之前的订阅,优先选择试用套餐 if (selectedSubscribe == null) { - KRLogUtil.kr_i('开始查找已购买的套餐...', tag: 'SubscribeService'); + KRLogUtil.kr_i('开始查找试用套餐...', tag: 'SubscribeService'); + + for (var subscribe in subscribes) { + KRLogUtil.kr_i('检查订阅: ${subscribe.name}(${subscribe.id}), 是否试用: ${subscribe.isTryOut}', + tag: 'SubscribeService'); + + if (subscribe.isTryOut) { + selectedSubscribe = subscribe; + KRLogUtil.kr_i('✅ 找到试用套餐,默认选择: ${selectedSubscribe.name}', + tag: 'SubscribeService'); + break; + } + } + } + + // 3. 如果没有试用套餐,选择已购买的套餐(非试用) + if (selectedSubscribe == null) { + KRLogUtil.kr_i('没有试用套餐,查找已购买的套餐...', tag: 'SubscribeService'); KRLogUtil.kr_i('已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', tag: 'SubscribeService'); @@ -524,10 +564,10 @@ class KRSubscribeService { } } - // 3. 如果没有已购买的套餐,选择第一个(可能是试用套餐) + // 4. 如果都没有找到,选择第一个 if (selectedSubscribe == null) { selectedSubscribe = subscribes.first; - KRLogUtil.kr_i('没有已购买的套餐,选择第一个: ${selectedSubscribe.name}', + KRLogUtil.kr_i('没有找到匹配的套餐,选择第一个: ${selectedSubscribe.name}', tag: 'SubscribeService'); } @@ -545,9 +585,13 @@ class KRSubscribeService { (nodes) => nodes, ); - // 处理节点列表 + // 记录当前节点列表是否包含试用节点 + kr_hasTrialNodes.value = nodes.isTryOut; + KRLogUtil.kr_i('节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService'); + + // 处理节点列表(不使用分组) final listModel = KrOutboundsList(); - listModel.processOutboundItems(nodes.list, kr_nodeGroups); + listModel.processOutboundItems(nodes.list, []); // 更新UI数据 groupOutboundList.value = listModel.groupOutboundList; @@ -596,6 +640,7 @@ class KRSubscribeService { Future kr_clearCutNodeData() async { kr_isLastDayOfSubscription.value = false; kr_isTrial.value = false; + kr_hasTrialNodes.value = false; kr_subscriptionRemainingTime.value = ''; kr_trialRemainingTime.value = ''; @@ -618,4 +663,45 @@ class KRSubscribeService { /// 获取当前订阅 KRUserAvailableSubscribeItem? get kr_getCurrentSubscribe => kr_currentSubscribe.value; + + /// 获取免费节点列表(试用节点) + /// 当试用结束时,返回空列表 + List get kr_freeNodes { + if (!kr_hasTrialNodes.value) { + return []; + } + + // 如果当前是试用状态或有试用节点,返回所有节点作为免费节点 + // 如果试用已结束(kr_isTrial=false 且 kr_hasTrialNodes=true),返回空列表 + if (kr_isTrial.value) { + KRLogUtil.kr_i('返回免费节点: ${allList.length} 个', tag: 'SubscribeService'); + return allList.toList(); + } else { + KRLogUtil.kr_i('试用已结束,不显示免费节点', tag: 'SubscribeService'); + return []; + } + } + + /// 获取付费节点列表 + /// 如果当前不是试用,返回所有节点 + /// 如果当前是试用,返回空列表 + List get kr_paidNodes { + if (kr_isTrial.value) { + KRLogUtil.kr_i('当前是试用状态,不显示付费节点', tag: 'SubscribeService'); + return []; + } else { + KRLogUtil.kr_i('返回付费节点: ${allList.length} 个', tag: 'SubscribeService'); + return allList.toList(); + } + } + + /// 是否应该显示免费标签页 + bool get kr_shouldShowFreeTab { + return kr_hasTrialNodes.value && kr_isTrial.value; + } + + /// 是否应该显示付费标签页 + bool get kr_shouldShowPaidTab { + return !kr_isTrial.value; + } }