Merge branch 'release_sky' into 'master_sky'

Release sky

See merge request StarlockTeam/app-starlock!272
This commit is contained in:
李仪 2025-09-03 09:46:21 +00:00
commit 46c9bc7a29
84 changed files with 45667 additions and 44745 deletions

View File

@ -1160,5 +1160,11 @@
"锁语音包设置": "قفل مجموعة صوت",
"(中国台湾)": "(中国台湾)",
"男声": "ذكر صوت",
"女声": "صوت بنات"
"女声": "صوت بنات",
"您的图像和视频数据仅保留": "يتم الاحتفاظ ببيانات الصور والفيديو فقط",
"后图像和视频数据将会失效,开通": "بعد ذلك ، ستكون بيانات الصورة والفيديو غير صالحة ويتم تنشيطها",
"云存会员": "عضوية التخزين السحابي",
"服务,图像视频信息随心存!": "معلومات الخدمة والصور والفيديو في قلبك!",
"图像": "صورة",
"视频": "فيديو"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Заключване на настройките на гласовия пакет",
"(中国台湾)": "(中国台湾)",
"男声": "Мъжки глас",
"女声": "Женски глас"
"女声": "Женски глас",
"您的图像和视频数据仅保留": "Данните ви за изображения и видеоклипове се запазват само",
"后图像和视频数据将会失效,开通": "След това данните за изображението и видеото ще бъдат невалидни и активирани",
"云存会员": "Членство в Cloud Storage",
"服务,图像视频信息随心存!": "Информацията за обслужване, изображения и видео са във вашето сърце!",
"图像": "изображение",
"视频": "Видео"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "ভয়েস প্যাকেজ সেটিংস লক করুন",
"(中国台湾)": "(中国台湾)",
"男声": "পুরুষের কণ্ঠ",
"女声": "নারী কণ্ঠ"
"女声": "নারী কণ্ঠ",
"您的图像和视频数据仅保留": "আপনার চিত্র এবং ভিডিও ডেটা কেবল ধরে রাখা হয়",
"后图像和视频数据将会失效,开通": "এর পরে, চিত্র এবং ভিডিও ডেটা অবৈধ এবং সক্রিয় হবে",
"云存会员": "ক্লাউড স্টোরেজ সদস্যতা",
"服务,图像视频信息随心存!": "পরিষেবা, চিত্র এবং ভিডিও তথ্য আপনার হৃদয়ে!",
"图像": "প্রতিচ্ছবি",
"视频": "ভিডিও"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Zamknout nastavení hlasového balíčku",
"(中国台湾)": "(中国台湾)",
"男声": "Mužský hlas",
"女声": "Ženský hlas"
"女声": "Ženský hlas",
"您的图像和视频数据仅保留": "Uchovávají se pouze vaše obrazová data a data videí",
"后图像和视频数据将会失效,开通": "Poté budou obrazová a video data neplatná a aktivovaná",
"云存会员": "Členství v cloudovém úložišti",
"服务,图像视频信息随心存!": "Servis, obrazové a video informace jsou na prvním místě!",
"图像": "obraz",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Lås stemmepakkeindstillinger",
"(中国台湾)": "(中国台湾)",
"男声": "Mandlige stemmer",
"女声": "Kvindelige stemmer"
"女声": "Kvindelige stemmer",
"您的图像和视频数据仅保留": "Dine billed- og videodata opbevares kun",
"后图像和视频数据将会失效,开通": "Derefter vil billed- og videodataene være ugyldige og aktiveret",
"云存会员": "Medlemskab af Cloud Storage",
"服务,图像视频信息随心存!": "Service-, billed- og videoinformation er i dit hjerte!",
"图像": "billede",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Sperren von Sprachpaketeinstellungen",
"(中国台湾)": "(中国台湾)",
"男声": "Männliche Stimme",
"女声": "Frauenstimme"
"女声": "Frauenstimme",
"您的图像和视频数据仅保留": "Ihre Bild- und Videodaten werden nur dann aufbewahrt",
"后图像和视频数据将会失效,开通": "Danach sind die Bild- und Videodaten ungültig und aktiviert",
"云存会员": "Cloud-Speicher-Mitgliedschaft",
"服务,图像视频信息随心存!": "Service-, Bild- und Videoinformationen liegen Ihnen am Herzen!",
"图像": "Bild",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Ρυθμίσεις κλειδώματος πακέτου φωνής",
"(中国台湾)": "(中国台湾)",
"男声": "Ανδρική φωνή",
"女声": "Γυναικεία φωνή"
"女声": "Γυναικεία φωνή",
"您的图像和视频数据仅保留": "Τα δεδομένα εικόνας και βίντεο διατηρούνται μόνο",
"后图像和视频数据将会失效,开通": "Μετά από αυτό, τα δεδομένα εικόνας και βίντεο θα είναι άκυρα και θα ενεργοποιηθούν",
"云存会员": "Συνδρομή Cloud Storage",
"服务,图像视频信息随心存!": "Οι πληροφορίες εξυπηρέτησης, εικόνας και βίντεο είναι στην καρδιά σας!",
"图像": "εικόνα",
"视频": "Βίντεο"
}

View File

@ -1167,5 +1167,11 @@
"锁语音包设置": "Lock voice package settings",
"(中国台湾)": "(中国台湾)",
"男声": "male voice",
"女声": "female voice"
"女声": "female voice",
"您的图像和视频数据仅保留": "Your image and video data is only retained",
"后图像和视频数据将会失效,开通": "After that, the image and video data will be invalid and activated",
"云存会员": "Cloud Storage Membership",
"服务,图像视频信息随心存!": "Service, image and video information are at your heart!",
"图像": "image",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Configuración del paquete de voz de bloqueo",
"(中国台湾)": "(中国台湾)",
"男声": "Voz masculina",
"女声": "Voz femenina"
"女声": "Voz femenina",
"您的图像和视频数据仅保留": "Solo se conservan los datos de imagen y vídeo",
"后图像和视频数据将会失效,开通": "Después de eso, los datos de imagen y video no serán válidos y se activarán",
"云存会员": "Membresía de almacenamiento en la nube",
"服务,图像视频信息随心存!": "¡La información de servicio, imagen y video está en su corazón!",
"图像": "imagen",
"视频": "Vídeo"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Lukustage häälepaketi seaded",
"(中国台湾)": "(中国台湾)",
"男声": "Meeste hääl",
"女声": "Naiste hääl"
"女声": "Naiste hääl",
"您的图像和视频数据仅保留": "Teie pildi- ja videoandmeid säilitatakse ainult",
"后图像和视频数据将会失效,开通": "Pärast seda on pildi- ja videoandmed kehtetud ja aktiveeritud",
"云存会员": "Pilvesalvestuse liikmelisus",
"服务,图像视频信息随心存!": "Teenindus-, pildi- ja videoteave on teie südames!",
"图像": "Piltide",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Lukitse äänipaketin asetukset",
"(中国台湾)": "(中国台湾)",
"男声": "Miehen ääni",
"女声": "Naisten ääni"
"女声": "Naisten ääni",
"您的图像和视频数据仅保留": "Kuva- ja videotietosi säilytetään vain",
"后图像和视频数据将会失效,开通": "Sen jälkeen kuva- ja videotiedot ovat virheellisiä ja aktivoituvat",
"云存会员": "Pilvitallennustilan jäsenyys",
"服务,图像视频信息随心存!": "Palvelu-, kuva- ja videotiedot ovat sydämessäsi!",
"图像": "kuva",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Paramètres du pack Lock Voice",
"(中国台湾)": "(中国台湾)",
"男声": "Voix masculine",
"女声": "Voix de femmes"
"女声": "Voix de femmes",
"您的图像和视频数据仅保留": "Vos données dimage et de vidéo ne sont conservées que",
"后图像和视频数据将会失效,开通": "Après cela, les données de limage et de la vidéo seront invalides et activées",
"云存会员": "Adhésion au stockage dans le cloud",
"服务,图像视频信息随心存!": "Le service, limage et les informations vidéo sont au cœur de vos préoccupations !",
"图像": "image",
"视频": "Vidéo"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "הגדרות חבילת קול לנעול",
"(中国台湾)": "(中国台湾)",
"男声": "קול גבר",
"女声": "קול נשי"
"女声": "קול נשי",
"您的图像和视频数据仅保留": "נתוני התמונה והסרטונים נשמרים רק",
"后图像和视频数据将会失效,开通": "לאחר מכן, נתוני התמונה והווידאו לא יהיו חוקיים ויופעלו",
"云存会员": "חברות באחסון בענן",
"服务,图像视频信息随心存!": "מידע על שירות, תמונה ווידאו נמצאים בלב שלך!",
"图像": "תמונה",
"视频": "וידאו"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "आवाज पैकेज सेटिंग्स ताला लगाएँ",
"(中国台湾)": "(中国台湾)",
"男声": "पुरुष आवाज",
"女声": "महिला आवाज"
"女声": "महिला आवाज",
"您的图像和视频数据仅保留": "आपकी छवि और वीडियो डेटा केवल बनाए रखा जाता है",
"后图像和视频数据将会失效,开通": "उसके बाद, छवि और वीडियो डेटा अमान्य और सक्रिय हो जाएगा",
"云存会员": "क्लाउड स्टोरेज सदस्यता",
"服务,图像视频信息随心存!": "सेवा, छवि और वीडियो जानकारी आपके दिल में हैं!",
"图像": "प्रतिबिंब",
"视频": "वीडियो"
}

View File

@ -1162,5 +1162,11 @@
"锁语音包设置": "鎖語音包設定",
"(中国台湾)": "(中国台湾)",
"男声": "男聲",
"女声": "女聲"
"女声": "女聲",
"您的图像和视频数据仅保留": "您的圖像和視頻數據僅保留",
"后图像和视频数据将会失效,开通": "后圖像和視頻數據將會失效,開通",
"云存会员": "雲存會員",
"服务,图像视频信息随心存!": "服務,圖像視頻資訊隨心存!",
"图像": "圖像",
"视频": "視頻"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Postavke zaključavanja glasovnog paketa",
"(中国台湾)": "(中国台湾)",
"男声": "Muški glas",
"女声": "Ženski glas"
"女声": "Ženski glas",
"您的图像和视频数据仅保留": "Vaši podaci o slici i videozapisu zadržavaju se samo",
"后图像和视频数据将会失效,开通": "Nakon toga, slikovni i video podaci bit će nevažeći i aktivirani",
"云存会员": "Članstvo u pohrani u oblaku",
"服务,图像视频信息随心存!": "Informacije o usluzi, slikama i videozapisima su vam u srcu!",
"图像": "slika",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Hangcsomag zárolási beállításai",
"(中国台湾)": "(中国台湾)",
"男声": "Férfi hang",
"女声": "női hang"
"女声": "női hang",
"您的图像和视频数据仅保留": "A kép- és videóadatokat csak a rendszer őrzi meg",
"后图像和视频数据将会失效,开通": "Ezt követően a kép- és videóadatok érvénytelenek lesznek és aktiválódnak",
"云存会员": "Felhőalapú tárolási tagság",
"服务,图像视频信息随心存!": "A szolgáltatás, a képi és videós információk a szívedben vannak!",
"图像": "kép",
"视频": "Video"
}

View File

@ -1167,5 +1167,11 @@
"锁语音包设置": "Փակել ձայնային փաթեթի պարամետրերը",
"(中国台湾)": "(中国台湾)",
"男声": "տղամարդկանց ձայն",
"女声": "կանանց ձայն"
"女声": "կանանց ձայն",
"您的图像和视频数据仅保留": "Ձեր պատկերի եւ վիդեո տվյալները պահպանվում են միայն",
"后图像和视频数据将会失效,开通": "Դրանից հետո պատկերի եւ վիդեո տվյալները կլինեն անվավեր եւ կակտիվացվեն",
"云存会员": "Cloud Storage Membership",
"服务,图像视频信息随心存!": "Ծառայությունը, պատկերը եւ վիդեո տեղեկատվությունը ձեր սրտում են:",
"图像": "Պատկերասրահ",
"视频": "Տեսանյութ"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Mengunci paket suara",
"(中国台湾)": "(中国台湾)",
"男声": "6 tahun sebelumnya",
"女声": "Suara wanita"
"女声": "Suara wanita",
"您的图像和视频数据仅保留": "Data gambar dan video Anda hanya disimpan",
"后图像和视频数据将会失效,开通": "Setelah itu, data gambar dan video akan tidak valid dan diaktifkan",
"云存会员": "Keanggotaan Cloud Storage",
"服务,图像视频信息随心存!": "Informasi layanan, gambar, dan video adalah inti Anda!",
"图像": "citra",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Impostazioni pacchetto vocale blocco",
"(中国台湾)": "(中国台湾)",
"男声": "voce maschile",
"女声": "voce femminile"
"女声": "voce femminile",
"您的图像和视频数据仅保留": "I dati delle immagini e dei video vengono conservati solo",
"后图像和视频数据将会失效,开通": "Successivamente, i dati dell'immagine e del video non saranno più validi e attivati",
"云存会员": "Iscrizione al cloud storage",
"服务,图像视频信息随心存!": "Le informazioni sul servizio, le immagini e i video sono al tuo centro!",
"图像": "immagine",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "ロック音声パケット設定",
"(中国台湾)": "(中国台湾)",
"男声": "男声",
"女声": "女声"
"女声": "女声",
"您的图像和视频数据仅保留": "画像と動画のデータのみが保持されます",
"后图像和视频数据将会失效,开通": "その後、画像とビデオのデータは無効になり、アクティブになります",
"云存会员": "クラウドストレージメンバーシップ",
"服务,图像视频信息随心存!": "サービス、画像、ビデオ情報があなたの中心にあります!",
"图像": "画像",
"视频": "ビデオ"
}

View File

@ -1167,5 +1167,11 @@
"锁语音包设置": "ხმის პაკეტის პარამეტრები",
"(中国台湾)": "(中国台湾)",
"男声": "მამაკაცის ხმა",
"女声": "ქალის ხმა"
"女声": "ქალის ხმა",
"您的图像和视频数据仅保留": "თქვენი სურათი და ვიდეო მონაცემები ინახება მხოლოდ",
"后图像和视频数据将会失效,开通": "ამის შემდეგ, სურათისა და ვიდეო მონაცემები არასწორი და გააქტიურებული იქნება",
"云存会员": "Cloud Storage წევრობა",
"服务,图像视频信息随心存!": "მომსახურება, სურათი და ვიდეო ინფორმაცია თქვენს გულშია!",
"图像": "სურათი",
"视频": "ვიდეო"
}

View File

@ -1172,5 +1172,11 @@
"语音包设置": "语音包设置",
"(中国台湾)": "(中国台湾)",
"男声": "男声",
"女声": "女声"
"女声": "女声",
"您的图像和视频数据仅保留": "您的图像和视频数据仅保留",
"后图像和视频数据将会失效,开通": "后图像和视频数据将会失效,开通",
"云存会员": "云存会员",
"服务,图像视频信息随心存!": "服务,图像视频信息随心存!",
"图像": "图像",
"视频": "视频"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Дауыстық бума параметрлерін құлыптау",
"(中国台湾)": "(中国台湾)",
"男声": "ер дауысы",
"女声": "Әйел дауысы"
"女声": "Әйел дауысы",
"您的图像和视频数据仅保留": "Сіздің кескініңіз бен бейне деректеріңіз тек сақталады",
"后图像和视频数据将会失效,开通": "Осыдан кейін кескін мен бейне деректер жарамсыз болып, белсендіріледі",
"云存会员": "Бұлтты сақтауға мүшелік",
"服务,图像视频信息随心存!": "Қызмет, бейне және бейне ақпарат сіздің жүрегіңізде жатыр!",
"图像": "кескіні",
"视频": "Бейне"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "음성팩 설정 잠금",
"(中国台湾)": "(中国台湾)",
"男声": "남성",
"女声": "여성 목소리"
"女声": "여성 목소리",
"您的图像和视频数据仅保留": "이미지 및 동영상 데이터만 보존됩니다.",
"后图像和视频数据将会失效,开通": "그 후 이미지 및 비디오 데이터는 유효하지 않고 활성화됩니다",
"云存会员": "클라우드 스토리지 멤버십",
"服务,图像视频信息随心存!": "서비스, 이미지 및 비디오 정보가 당신의 중심에 있습니다!",
"图像": "이미지",
"视频": "비디오"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Balso paketo nustatymų užrakinimas",
"(中国台湾)": "(中国台湾)",
"男声": "vyriškas balsas",
"女声": "Moteriškas balsas"
"女声": "Moteriškas balsas",
"您的图像和视频数据仅保留": "Vaizdo ir vaizdo įrašų duomenys saugomi tik",
"后图像和视频数据将会失效,开通": "Po to vaizdo ir vaizdo duomenys bus negaliojantys ir suaktyvinti",
"云存会员": "Debesies saugyklos narystė",
"服务,图像视频信息随心存!": "Aptarnavimas, vaizdas ir video informacija yra jūsų širdis!",
"图像": "vaizdas",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Balso paketo nustatymų užrakinimas",
"(中国台湾)": "(中国台湾)",
"男声": "vyriškas balsas",
"女声": "Moteriškas balsas"
"女声": "Moteriškas balsas",
"您的图像和视频数据仅保留": "Data imej dan video anda hanya dikekalkan",
"后图像和视频数据将会失效,开通": "Selepas itu, data imej dan video akan menjadi tidak sah dan diaktifkan",
"云存会员": "Keahlian Storan Awan",
"服务,图像视频信息随心存!": "Maklumat perkhidmatan, imej dan video adalah di hati anda!",
"图像": "Imej",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Instellingen voor spraakpakket vergrendelen",
"(中国台湾)": "(中国台湾)",
"男声": "mannelijke stem",
"女声": "Vrouwelijke stem"
"女声": "Vrouwelijke stem",
"您的图像和视频数据仅保留": "Uw beeld- en videogegevens worden alleen bewaard",
"后图像和视频数据将会失效,开通": "Daarna zijn de afbeeldings- en videogegevens ongeldig en geactiveerd",
"云存会员": "Lidmaatschap voor cloudopslag",
"服务,图像视频信息随心存!": "Service-, beeld- en video-informatie staan bij u centraal!",
"图像": "beeld",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Ustawienia blokowania pakietu głosowego",
"(中国台湾)": "(中国台湾)",
"男声": "Mężczyzna",
"女声": "Głos kobiecy"
"女声": "Głos kobiecy",
"您的图像和视频数据仅保留": "Dane obrazu i filmu są przechowywane tylko",
"后图像和视频数据将会失效,开通": "Po tym czasie dane obrazu i wideo zostaną nieważne i aktywowane",
"云存会员": "Członkostwo w usłudze Cloud Storage",
"服务,图像视频信息随心存!": "Informacje o serwisie, obrazie i wideo są w Twoim sercu!",
"图像": "obraz",
"视频": "Wideo"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Configurações do pacote de voz bloqueada",
"(中国台湾)": "(中国台湾)",
"男声": "Voz masculina",
"女声": "voz feminina"
"女声": "voz feminina",
"您的图像和视频数据仅保留": "Seus dados de imagem e vídeo são retidos apenas",
"后图像和视频数据将会失效,开通": "Depois disso, os dados de imagem e vídeo serão inválidos e ativados",
"云存会员": "Associação de armazenamento em nuvem",
"服务,图像视频信息随心存!": "Informações de serviço, imagem e vídeo estão no seu coração!",
"图像": "imagem",
"视频": "Vídeo"
}

View File

@ -1166,5 +1166,11 @@
"语音包设置": "Configurações do pacote de voz",
"(中国台湾)": "(中国台湾)",
"男声": "Macho",
"女声": "Garota"
"女声": "Garota",
"您的图像和视频数据仅保留": "Seus dados de imagem e vídeo são retidos apenas",
"后图像和视频数据将会失效,开通": "Depois disso, os dados de imagem e vídeo serão inválidos e ativados",
"云存会员": "Associação de armazenamento em nuvem",
"服务,图像视频信息随心存!": "Informações de serviço, imagem e vídeo estão no seu coração!",
"图像": "imagem",
"视频": "Vídeo"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Configurarea pachetului vocal de blocare",
"(中国台湾)": "(中国台湾)",
"男声": "vocea bărbatului",
"女声": "Voce feminină"
"女声": "Voce feminină",
"您的图像和视频数据仅保留": "Datele tale de imagine și video sunt păstrate numai",
"后图像和视频数据将会失效,开通": "După aceea, datele de imagine și video vor fi invalide și activate",
"云存会员": "Abonament de stocare în cloud",
"服务,图像视频信息随心存!": "Serviciile, imaginile și informațiile video sunt în centrul dumneavoastră!",
"图像": "imagine",
"视频": "Video"
}

View File

@ -1165,5 +1165,11 @@
"锁语音包设置": "Запустить настройки голосового пакета",
"(中国台湾)": "(中国台湾)",
"男声": "Мужской голос",
"女声": "Женские голоса"
"女声": "Женские голоса",
"您的图像和视频数据仅保留": "Ваши изображения и видеоданные сохраняются только",
"后图像和视频数据将会失效,开通": "После этого изображение и видео данные будут недействительными и активированы",
"云存会员": "Членство в облачном хранилище",
"服务,图像视频信息随心存!": "Сервисная, имиджевая и видеоинформация в Вашем сердце!",
"图像": "образ",
"视频": "Видео"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Zamknutie nastavení hlasového balíka",
"(中国台湾)": "(中国台湾)",
"男声": "mužský hlas",
"女声": "Ženský hlas"
"女声": "Ženský hlas",
"您的图像和视频数据仅保留": "Vaše údaje o obrázkoch a videách sa zachovajú iba",
"后图像和视频数据将会失效,开通": "Potom budú údaje o obrázku a videu neplatné a aktivované",
"云存会员": "Členstvo v cloudovom úložisku",
"服务,图像视频信息随心存!": "Informácie o službách, obrázkoch a videách sú vo vašom srdci!",
"图像": "obraz",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Закључајте подешавања говорног пакета",
"(中国台湾)": "(中国台湾)",
"男声": "мушки глас",
"女声": "женски глас"
"女声": "женски глас",
"您的图像和视频数据仅保留": "Ваши подаци о слици и видео записима се задржавају само",
"后图像和视频数据将会失效,开通": "Након тога, сликовни и видео подаци ће бити неважећи и активирани",
"云存会员": "Чланство у облаку за складиштење",
"服务,图像视频信息随心存!": "Сервис , слике и видео информације су у вашем срцу!",
"图像": "Слика",
"视频": "Пријава"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Lås inställningar för röstpaket",
"(中国台湾)": "(中国台湾)",
"男声": "Mänsklig röst",
"女声": "Kvinnlig röst"
"女声": "Kvinnlig röst",
"您的图像和视频数据仅保留": "Dina bild- och videodata sparas endast",
"后图像和视频数据将会失效,开通": "Efter det kommer bild- och videodata att vara ogiltiga och aktiverade",
"云存会员": "Medlemskap i molnlagring",
"服务,图像视频信息随心存!": "Service, bild- och videoinformation finns i ditt hjärta!",
"图像": "bild",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "ล็อคการตั้งค่า Voice Pack",
"(中国台湾)": "(中国台湾)",
"男声": "เสียงผู้ชาย",
"女声": "เสียงผู้หญิง"
"女声": "เสียงผู้หญิง",
"您的图像和视频数据仅保留": "ระบบจะเก็บข้อมูลรูปภาพและวิดีโอของคุณไว้เท่านั้น",
"后图像和视频数据将会失效,开通": "หลังจากนั้น ข้อมูลรูปภาพและวิดีโอจะไม่ถูกต้องและเปิดใช้งาน",
"云存会员": "สมาชิกที่เก็บข้อมูลบนคลาวด์",
"服务,图像视频信息随心存!": "ข้อมูลบริการ รูปภาพ และวิดีโออยู่ที่หัวใจของคุณ!",
"图像": "ภาพ",
"视频": "วีดิทัศน์"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Ses Paketi Ayarlarını Kilitle",
"(中国台湾)": "(中国台湾)",
"男声": "Erkek Sesi",
"女声": "Kadın Sesi"
"女声": "Kadın Sesi",
"您的图像和视频数据仅保留": "Görüntü ve video verileriniz yalnızca korunur",
"后图像和视频数据将会失效,开通": "Bundan sonra, görüntü ve video verileri geçersiz olacak ve etkinleştirilecektir",
"云存会员": "Bulut Depolama Üyeliği",
"服务,图像视频信息随心存!": "Servis, görüntü ve video bilgileri kalbinizde!",
"图像": "resim",
"视频": "Video"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "鎖語音包設定",
"(中国台湾)": "(中国台湾)",
"男声": "男聲",
"女声": "女聲"
"女声": "女聲",
"您的图像和视频数据仅保留": "您的圖像和視頻數據僅保留",
"后图像和视频数据将会失效,开通": "后圖像和視頻數據將會失效,開通",
"云存会员": "雲存會員",
"服务,图像视频信息随心存!": "服務,圖像視頻資訊隨心存!",
"图像": "圖像",
"视频": "視頻"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Параметри блокування голосового пакету",
"(中国台湾)": "(中国台湾)",
"男声": "Чоловічий голос",
"女声": "жіночий голос"
"女声": "жіночий голос",
"您的图像和视频数据仅保留": "Ваші зображення та відеодані зберігаються лише",
"后图像和视频数据将会失效,开通": "Після цього дані зображення та відео будуть недійсними та активованими",
"云存会员": "Членство в хмарних сховищах",
"服务,图像视频信息随心存!": "Сервіс, зображення та відео інформація у вашому серці!",
"图像": "образ",
"视频": "Відео"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "صوتی پیکیج کی ترتیبات لاک کریں",
"(中国台湾)": "(中国台湾)",
"男声": "مردوں کی آواز",
"女声": "خواتین کی آواز"
"女声": "خواتین کی آواز",
"您的图像和视频数据仅保留": "آپ کی تصویر اور ویڈیو کا ڈیٹا صرف برقرار رکھا گیا ہے",
"后图像和视频数据将会失效,开通": "اس کے بعد ، تصویر اور ویڈیو کا ڈیٹا غیر قانونی اور فعال ہوجائے گا۔",
"云存会员": "Cloud Storage Membership",
"服务,图像视频信息随心存!": "خدمت، تصویر اور ویڈیو کی معلومات آپ کے دل میں ہیں!",
"图像": "روپ",
"视频": "ویڈیو"
}

View File

@ -1161,5 +1161,11 @@
"锁语音包设置": "Khóa cài đặt gói thoại",
"(中国台湾)": "(中国台湾)",
"男声": "Giọng nam",
"女声": "Giọng nữ"
"女声": "Giọng nữ",
"您的图像和视频数据仅保留": "Dữ liệu hình ảnh và video của bạn chỉ được giữ lại",
"后图像和视频数据将会失效,开通": "Sau đó, dữ liệu hình ảnh và video sẽ không hợp lệ và được kích hoạt",
"云存会员": "Tư cách thành viên lưu trữ đám mây",
"服务,图像视频信息随心存!": "Thông tin dịch vụ, hình ảnh và video là trọng tâm của bạn!",
"图像": "ảnh",
"视频": "Video"
}

View File

@ -63,7 +63,6 @@
"授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人": "授权管理员拥有操作这把锁的重要权限,请确保只发给我你信任的人",
"功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。": "功能开启后,你将可以通过网关远程开锁。此功能的开启和关闭只能在锁附近通过手机蓝牙进行。",
"此功能的开启和关闭只能在锁附近通过手机蓝牙进行": "此功能的开启和关闭只能在锁附近通过手机蓝牙进行",
"功能开启后,你将可以通过网关远程开锁。": "功能开启后,你将可以通过网关远程开锁。",
"排列方式": "排列方式",
"早到榜": "早到榜",
@ -1174,5 +1173,11 @@
"语音包设置": "语音包设置",
"(中国台湾)": "(中国台湾)",
"男声": "男声",
"女声": "女声"
"女声": "女声",
"您的图像和视频数据仅保留": "您的图像和视频数据仅保留",
"后图像和视频数据将会失效,开通": "后图像和视频数据将会失效,开通",
"云存会员": "云存会员",
"服务,图像视频信息随心存!": "服务,图像视频信息随心存!",
"图像": "图像",
"视频": "视频"
}

View File

@ -0,0 +1,54 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../io_reply.dart';
import '../io_sender.dart';
import '../io_tool/io_tool.dart';
import '../io_type.dart';
import '../sm4Encipher/sm4.dart';
//oat升级
class ReadLockCurrentVoicePacket extends SenderProtocol {
ReadLockCurrentVoicePacket({
this.lockID,
}) : super(CommandType.readLockCurrentVoicePacket);
String? lockID;
@override
String toString() {
return 'ReadLockCurrentVoicePacket{lockID: $lockID}';
}
@override
List<int> messageDetail() {
List<int> data = <int>[];
//
final int type = commandType!.typeValue;
final double typeDouble = type / 256;
final int type1 = typeDouble.toInt();
final int type2 = type % 256;
data.add(type1);
data.add(type2);
// id 40
final int lockIDLength = utf8.encode(lockID!).length;
data.addAll(utf8.encode(lockID!));
data = getFixedLengthList(data, 40 - lockIDLength);
printLog(data);
return data;
}
}
class ReadLockCurrentVoicePacketReply extends Reply {
ReadLockCurrentVoicePacketReply.parseData(
CommandType commandType, List<int> dataDetail)
: super.parseData(commandType, dataDetail) {
data = dataDetail;
status = data[2];
errorWithStstus(status);
}
}

View File

@ -0,0 +1,61 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../io_reply.dart';
import '../io_sender.dart';
import '../io_tool/io_tool.dart';
import '../io_type.dart';
import '../sm4Encipher/sm4.dart';
//oat升级
class SetVoicePackageFinalResult extends SenderProtocol {
SetVoicePackageFinalResult({
this.lockID,
this.languageCode,
}) : super(CommandType.setLockCurrentVoicePacket);
String? lockID;
String? languageCode;
@override
String toString() {
return 'SetVoicePackageFinalResult{lockID: $lockID, languageCode: $languageCode}';
}
@override
List<int> messageDetail() {
List<int> data = <int>[];
//
final int type = commandType!.typeValue;
final double typeDouble = type / 256;
final int type1 = typeDouble.toInt();
final int type2 = type % 256;
data.add(type1);
data.add(type2);
// id 40
final int lockIDLength = utf8.encode(lockID!).length;
data.addAll(utf8.encode(lockID!));
data = getFixedLengthList(data, 40 - lockIDLength);
//languageCode 20
final int languageCodeLength = utf8.encode(languageCode!).length;
data.addAll(utf8.encode(languageCode!));
data = getFixedLengthList(data, 20 - languageCodeLength);
printLog(data);
return data;
}
}
class SetVoicePackageFinalResultReply extends Reply {
SetVoicePackageFinalResultReply.parseData(
CommandType commandType, List<int> dataDetail)
: super.parseData(commandType, dataDetail) {
data = dataDetail;
status = data[2];
errorWithStstus(status);
}
}

View File

@ -44,7 +44,9 @@ enum CommandType {
startVoicePackageConfigure, // 0x30A1
voicePackageConfigureProcess, // 0x30A2
voicePackageConfigureConfirmation, // 0x30A3
getDeviceModel, // 0x30A4
readLockCurrentVoicePacket, // 0x30A4
setLockCurrentVoicePacket, // 0x30A5
getDeviceModel, // 0x30A4
gatewayConfiguringWifi, // 0x30F4
gatewayConfiguringWifiResult, // 0x30F5
@ -210,7 +212,12 @@ extension ExtensionCommandType on CommandType {
break;
case 0x30A4:
{
type = CommandType.getDeviceModel;
type = CommandType.readLockCurrentVoicePacket;
}
break;
case 0x30A5:
{
type = CommandType.setLockCurrentVoicePacket;
}
break;
case 0x30F4:
@ -340,9 +347,12 @@ extension ExtensionCommandType on CommandType {
case CommandType.voicePackageConfigureConfirmation:
type = 0x30A3;
break;
case CommandType.getDeviceModel:
case CommandType.readLockCurrentVoicePacket:
type = 0x30A4;
break;
case CommandType.setLockCurrentVoicePacket:
type = 0x30A5;
break;
default:
type = 0x300A;
break;
@ -362,7 +372,8 @@ extension ExtensionCommandType on CommandType {
case CommandType.gatewayGetWifiList:
case CommandType.gatewayConfiguringWifi:
case CommandType.gatewayGetStatus:
case CommandType.getDeviceModel:
case CommandType.readLockCurrentVoicePacket:
case CommandType.setLockCurrentVoicePacket:
//
type = 0x20;
break;
@ -476,7 +487,10 @@ extension ExtensionCommandType on CommandType {
t = '语音包配置确认';
break;
case 0x30A4:
t = '获取设备型号';
t = '读取锁当前语音包';
break;
case 0x30A5:
t = '设置锁当前语音包';
break;
default:
t = '读星锁状态信息';

View File

@ -18,9 +18,11 @@ import 'package:star_lock/blue/io_protocol/io_processOtaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_readAdminPassword.dart';
import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsNoParameters.dart';
import 'package:star_lock/blue/io_protocol/io_readSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_referEventRecordTime.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsNoParameters.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_timing.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
@ -317,6 +319,18 @@ class CommandReciverManager {
commandType, data);
}
break;
case CommandType.readLockCurrentVoicePacket:
{
reply =
ReadLockCurrentVoicePacketReply.parseData(commandType, data);
}
break;
case CommandType.setLockCurrentVoicePacket:
{
reply =
SetVoicePackageFinalResultReply.parseData(commandType, data);
}
break;
case CommandType.generalExtendedCommond:
{
//

View File

@ -80,7 +80,11 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
),
],
),
body: ListView(
body: GestureDetector(
onTap: (){
FocusScope.of(context).unfocus();
},
child: ListView(
padding: EdgeInsets.only(top: 120.h, left: 40.w, right: 40.w),
children: <Widget>[
Container(
@ -305,6 +309,7 @@ class _StarLockLoginPageState extends State<StarLockLoginPage> {
],
),
],
),
));
}

View File

@ -62,10 +62,8 @@ FutureOr<void> main() async {
}
});
// //ToDo:
// runApp(MultiProvider(providers: [
// ChangeNotifierProvider(create: (_) => DebugInfoModel()),
// ], child: MyApp(isLogin: isLogin)));
// ios则初始化获取到voip token
// token callkit
if (Platform.isIOS) {
CallKitHandler.setupListener();
String? token = await CallKitHandler.getVoipToken();
@ -111,20 +109,4 @@ Future<void> privacySDKInitialization() async {
await jpushProvider.initJPushService();
NotificationService().init(); //
// /// ip如果属于国内才进行初始化
// final CheckIPEntity entity = await ApiRepository.to.checkIpAction(ip: '');
// if (entity.errorCode!.codeIsSuccessful) {
// String currentLanguage =
// CurrentLocaleTool.getCurrentLocaleString(); //
// // ip是国内的且选的是中文才初始化一键登录
// if (entity.data!.abbreviation?.toLowerCase() == 'cn' &&
// currentLanguage == 'zh_CN') {
// //
// final StarLockLoginLogic loginLogic = Get.put(StarLockLoginLogic());
// await JverifyOneClickLoginManage();
// loginLogic.state.isCheckVerifyEnable.value =
// await JverifyOneClickLoginManage().checkVerifyEnable();
// eventBus.fire(AgreePrivacyAgreement());
// }
// }
}

View File

@ -0,0 +1,11 @@
extension DateTimeExtensions on DateTime {
/// DateTime 00:00:00.000
DateTime get withoutTime {
return DateTime(year, month, day);
}
///
bool isSameDate(DateTime other) {
return year == other.year && month == other.month && day == other.day;
}
}

View File

@ -125,4 +125,9 @@ class DoorLockLogDataItem {
data['recordDetailStr'] = recordDetailStr;
return data;
}
@override
String toString() {
return 'DoorLockLogDataItem{recordId: $recordId, lockId: $lockId, lockAlias: $lockAlias, recordType: $recordType, recordTypeName: $recordTypeName, username: $username, operateDate: $operateDate, imagesUrl: $imagesUrl, videoUrl: $videoUrl, headUrl: $headUrl, userid: $userid, keyboardPwd: $keyboardPwd, recordStr: $recordStr, recordDetailStr: $recordDetailStr}';
}
}

View File

@ -3,11 +3,15 @@ import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';
import 'package:star_lock/apm/apm_helper.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
import 'package:star_lock/main/lockDetail/lockOperatingRecord/lockOperatingRecordGetLastRecordTime_entity.dart';
import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_entity.dart';
import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/dateTool.dart';
import 'package:star_lock/tools/eventBusEventManage.dart';
@ -235,13 +239,15 @@ class DoorLockLogLogic extends BaseGetXController {
lockId: state.keyInfos.value.lockId!,
lockEventType: state.dropdownValue.value,
pageNo: pageNo,
pageSize: int.parse(pageSize),
pageSize: 1000,
startDate: state.startDate.value,
endDate: state.endDate.value);
if (entity.errorCode!.codeIsSuccessful) {
//
state.lockLogItemList.addAll(entity.data!.itemList!);
state.lockLogItemList.refresh();
state.weekEventList.addAll(entity.data!.itemList!);
state.weekEventList.refresh();
//
pageNo++;
}
@ -358,6 +364,7 @@ class DoorLockLogLogic extends BaseGetXController {
@override
Future<void> onInit() async {
_setWeekRange();
super.onInit();
//
@ -370,6 +377,48 @@ class DoorLockLogLogic extends BaseGetXController {
}
}
void _setWeekRange() {
final now = DateTime.now();
// 1=7=
int weekday = now.weekday; // 1-7
//
// : 0, : 1, ..., : 6
int daysToSubtract = weekday - 1; // 1
// 00:00:00.000
DateTime startOfWeek = DateTime(now.year, now.month, now.day)
.subtract(Duration(days: daysToSubtract));
// 23:59:59.999
DateTime endOfWeek = startOfWeek
.add(Duration(days: 6)) // 6
.add(Duration(hours: 23, minutes: 59, seconds: 59, milliseconds: 999));
//
state.startDate.value = startOfWeek.millisecondsSinceEpoch;
state.endDate.value = endOfWeek.millisecondsSinceEpoch;
}
//
void refreshWeek() {
_setWeekRange();
}
getWebPlayUrl() async {
final AdvancedFeaturesWebEntity entity =
await ApiRepository.to.getServicePackageBuyUrl();
if (entity.errorCode!.codeIsSuccessful) {
state.cloudStorageWebViewUrl.value = entity.data!.cloudStorage!;
final uploadReportBuyRequest = await ApiRepository.to
.uploadReportBuyRequest(lockId: state.keyInfos.value.lockId!);
if (uploadReportBuyRequest.errorCode!.codeIsSuccessful) {
Get.toNamed(Routers.advancedFeaturesWebPage, arguments: <String, int>{
'webBuyType': XSConstantMacro.webBuyTypeCloudStorage,
});
}
}
}
@override
Future<void> onClose() async {
super.onClose();

View File

@ -1,14 +1,18 @@
import 'package:flustars/flustars.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_logic.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_state.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/exportRecordDialog/exportRecordDialog_page.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/week_calendar_view.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/widget/full_screenImage_page.dart';
import 'package:star_lock/main/lockDetail/videoLog/widget/video_thumbnail_image.dart';
@ -34,8 +38,39 @@ class DoorLockLogPage extends StatefulWidget {
}
class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
final ScrollController _scrollController = ScrollController();
final DoorLockLogLogic logic = Get.put(DoorLockLogLogic());
final DoorLockLogState state = Get.find<DoorLockLogLogic>().state;
bool _isAtBottom = false;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
final max = _scrollController.position.maxScrollExtent;
final current = _scrollController.position.pixels;
AppLog.log('current:${current}');
// 5
if (current >= max - 5) {
if (!_isAtBottom) {
setState(() {
_isAtBottom = true;
});
print('✅ 已滑动到 timelines 列表底部!');
//
}
} else {
if (_isAtBottom) {
setState(() {
_isAtBottom = false;
});
}
}
}
@override
Widget build(BuildContext context) {
@ -97,16 +132,31 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
topAdvancedCalendarWidget(),
Divider(
height: 1,
color: AppColors.greyLineColor,
indent: 30.w,
endIndent: 30.w,
),
eventDropDownWidget(),
Expanded(child: timeLineView())
],
),
floatingActionButton: Visibility(
visible: _isAtBottom,
child: FloatingActionButton(
onPressed: () {
_scrollController.animateTo(
0.0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(48.w),
),
backgroundColor: AppColors.mainColor,
child: Icon(
Icons.arrow_upward,
color: Colors.white,
size: 48.w,
),
),
),
);
}
@ -152,88 +202,14 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
}
}
// switch (value) {
// case "读取记录".tr:
// {
// logic.mockNetworkDataRequest(isRefresh: true);
// }
// break;
// case '清空记录'.tr:
// {
// ShowCupertinoAlertView().showClearOperationRecordAlert(
// clearClick: () {
// logic.clearOperationRecordRequest();
// });
// }
// break;
// case '导出记录':
// {
// showDialog(
// context: context,
// builder: (BuildContext context) {
// return ExportRecordDialog(
// onExport: (String filePath) {
// Get.toNamed(Routers.exportSuccessPage,
// arguments: <String, String>{'filePath': filePath});
// },
// );
// },
// );
// }
// break;
// }
// }
//
Widget topAdvancedCalendarWidget() {
final ThemeData theme = Theme.of(context);
return Theme(
data: theme.copyWith(
textTheme: theme.textTheme.copyWith(
titleMedium: theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
color: theme.colorScheme.secondary,
),
bodyLarge: theme.textTheme.bodyLarge!.copyWith(
fontSize: 14,
color: Colors.black54,
),
bodyMedium: theme.textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: Colors.black87,
),
),
primaryColor: AppColors.mainColor,
highlightColor: Colors.yellow,
disabledColor: Colors.grey,
),
child: Stack(
children: <Widget>[
AdvancedCalendar(
controller: state.calendarControllerCustom,
events: state.events,
weekLineHeight: 48.0,
startWeekDay: 1,
innerDot: true,
keepLineSize: true,
calendarTextStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
height: 1.3125,
letterSpacing: 0,
),
),
Positioned(
top: 8.0,
right: 8.0,
child: Obx(() => Text(
'${state.currentSelectDate.value.year}${''.tr}${state.currentSelectDate.value.month}${''.tr}',
style: theme.textTheme.titleMedium!.copyWith(
fontSize: 16,
color: theme.colorScheme.secondary,
),
)),
),
return Container(
margin: EdgeInsets.only(top: 20.h, left: 30.w, bottom: 10.h, right: 20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildWeekCalendar(),
],
),
);
@ -269,15 +245,10 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
),
child: Obx(() => EasyRefreshTool(
onRefresh: () async {
logic.mockNetworkDataRequest(isRefresh: true);
},
onLoad: () async {
logic.mockNetworkDataRequest(isRefresh: false);
},
child: state.lockLogItemList.isNotEmpty
child: Obx(
() => state.lockLogItemList.isNotEmpty
? Timeline.tileBuilder(
controller: _scrollController,
builder: _timelineBuilderWidget(),
theme: TimelineThemeData(
nodePosition: 0.04, //
@ -293,14 +264,19 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
),
),
)
: NoData())),
: NoData(),
),
);
}
String formatTimestampToHHmm(int timestampMs) {
// 1. DateTime
int timestampSec = timestampMs ~/ 1000;
String formatTimestampToDateTimeYYYYMMDD(int timestampMs) {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
DateFormat formatter =
DateFormat('MM${''.tr}dd${''.tr}'); // 2025-08-18 14:30
return formatter.format(dateTime);
}
String formatTimestampToHHmm(int timestampMs) {
// 2. DateTime
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestampMs);
@ -309,6 +285,17 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
return formatter.format(dateTime);
}
bool _checkIsVideoOrImagesType(DoorLockLogDataItem item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
case 220:
return true;
default:
return false;
}
}
String _buildIDByType(DoorLockLogDataItem item) {
final recordType = item.recordType;
switch (recordType) {
@ -325,7 +312,8 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
return '${formatTimestampToHHmm(item.operateDate!)} ' +
'密码'.tr +
'开锁'.tr +
'${'昵称'.tr}${item.username}'+'${'密码'.tr}${item.keyboardPwd}';
'${'昵称'.tr}${item.username}' +
'${'密码'.tr}${item.keyboardPwd}';
case 30:
return '${formatTimestampToHHmm(item.operateDate!)} ' +
''.tr +
@ -431,6 +419,20 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
itemCount: state.lockLogItemList.length,
contentsBuilder: (BuildContext context, int index) {
final DoorLockLogDataItem timelineData = state.lockLogItemList[index];
// 👇 videoUrl build
int? firstVideoIndex = state.lockLogItemList
.indexWhere((item) => _checkIsVideoOrImagesType(item));
bool isInvalid = _checkIsVideoOrImagesType(timelineData) &&
((timelineData.imagesUrl == null &&
timelineData.videoUrl == null) ||
(timelineData.videoUrl == '' && timelineData.imagesUrl == ''));
String typeText = '';
if (timelineData.recordType == 130) {
typeText = '图像'.tr;
} else if (timelineData.recordType == 220) {
typeText = '视频'.tr;
}
return GestureDetector(
onTap: () {
Get.toNamed(
@ -444,20 +446,45 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'${formatTimestampToDateTimeYYYYMMDD(timelineData.operateDate!)}',
style: TextStyle(
fontSize: 20.sp,
)),
// 使 SingleChildScrollView
SingleChildScrollView(
scrollDirection: Axis.horizontal, //
child: Text(
_buildIDByType(timelineData),
child: RichText(
textAlign: TextAlign.left,
text: TextSpan(
style: TextStyle(
color: _buildTextColorByType(timelineData),
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
//
children: [
TextSpan(
text: _buildIDByType(timelineData) +
(isInvalid
? '${typeText}' +
'已失效'.tr +
''
: ''),
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Visibility(
visible: isInvalid,
child: Icon(
Icons.error,
size: 24.sp,
color: Colors.red,
),
),
),
],
),
maxLines: 1,
//
overflow: TextOverflow.ellipsis,
),
),
@ -474,8 +501,71 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
),
),
SizedBox(
height: 20.h,
height: 12.h,
),
Visibility(
visible: _checkIsVideoOrImagesType(timelineData) &&
index == firstVideoIndex,
child: GestureDetector(
onTap: () async {
await logic.getWebPlayUrl();
},
child: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.r)),
),
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
//
TextSpan(
text:
'${'您的图像和视频数据仅保留'.tr} ${state.rollingStorageDays.value} ${''.tr} ,${state.rollingStorageDays.value} ${''.tr} ${'后图像和视频数据将会失效,开通'.tr}',
style: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1.8,
),
),
// 🔥
TextSpan(
text: '云存会员'.tr,
style: TextStyle(
color: AppColors.mainColor,
fontSize: 22.sp,
fontWeight: FontWeight.w800,
//
decoration: TextDecoration.underline,
decorationThickness: 1.5,
height: 1.8,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
// 👉
print('点击了“云存会员”');
await logic.getWebPlayUrl();
// Navigator.push(context, MaterialPageRoute(builder: ...));
},
),
//
TextSpan(
text: '服务,图像视频信息随心存!'.tr,
style: TextStyle(
color: Colors.grey,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1.8,
),
),
],
),
),
),
),
)
],
),
),
@ -622,4 +712,56 @@ class _DoorLockLogPageState extends State<DoorLockLogPage> with RouteAware {
}
state.ifCurrentScreen.value = false;
}
List<DateTime> getCurrentWeekDates() {
final now = DateTime.now();
// weekday: 1=, 2=, ..., 7=
//
// weekday == 7 0
final int daysSinceSunday = now.weekday % 7; // =1 -> %7=1, =7 -> %7=0
final List<DateTime> weekDates = [];
for (int i = 0; i < 7; i++) {
final DateTime day = DateTime(
now.year,
now.month,
now.day - daysSinceSunday + i, //
);
weekDates.add(day);
}
return weekDates;
}
Widget _buildWeekCalendar() {
return Obx(() {
final list = state.weekEventList.value;
final dateSet = list
.map((e) => DateTime.fromMillisecondsSinceEpoch(e.operateDate!))
.map((dt) => dt.withoutTime) //
.toSet(); // Set
AppLog.log('dateSet:${dateSet}');
return WeekCalendarView(
hasData: (DateTime date) {
return dateSet.contains(date.withoutTime);
},
onDateSelected: (DateTime date) async {
print('外部收到选中: $date');
state.operateDate = date.millisecondsSinceEpoch;
state.startDate.value =
DateTime(date.year, date.month, date.day).millisecondsSinceEpoch;
state.endDate.value =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999)
.millisecondsSinceEpoch;
await logic.mockNetworkDataRequest(isRefresh: true);
},
onWeekChanged: (DateTime start, DateTime end) {
state.startDate.value = start.millisecondsSinceEpoch;
state.endDate.value = end.millisecondsSinceEpoch;
logic.mockNetworkDataRequest(isRefresh: true);
},
);
});
}
}

View File

@ -1,5 +1,5 @@
import 'package:get/get.dart';
import 'package:get/get_rx/get_rx.dart';
import 'package:star_lock/common/XSConstantMacro/XSConstantMacro.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/doorLockLog_entity.dart';
import 'package:star_lock/tools/advancedCalendar/src/controller.dart';
@ -13,10 +13,15 @@ class DoorLockLogState {
DoorLockLogState() {
keyInfos.value = Get.arguments['keyInfo'];
}
final Rx<DoorLockLogEntity> lockLogEntity = DoorLockLogEntity().obs;
final Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
final RxList<DoorLockLogDataItem> lockLogItemList =
<DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> weekEventList =
<DoorLockLogDataItem>[].obs;
final RxList<DoorLockLogDataItem> dayEventList =
<DoorLockLogDataItem>[].obs;
final AdvancedCalendarController calendarControllerToday =
AdvancedCalendarController.today();
final AdvancedCalendarController calendarControllerCustom =
@ -69,4 +74,6 @@ class DoorLockLogState {
int logCountPage = 10; //
Rx<DateTime> currentSelectDate = DateTime.now().obs;
bool isLockReceiveResponse = false; //
RxString cloudStorageWebViewUrl = ''.obs;
RxInt rollingStorageDays = 3.obs; //
}

View File

@ -0,0 +1,220 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/main/lockDetail/doorLockLog/date_time_extensions.dart';
class WeekCalendarView extends StatefulWidget {
//
final bool Function(DateTime date)? hasData;
final void Function(DateTime date)? onDateSelected; //
final void Function(DateTime start, DateTime end)? onWeekChanged;
const WeekCalendarView({
Key? key,
this.hasData,
this.onDateSelected,
this.onWeekChanged,
}) : super(key: key);
@override
_WeekCalendarViewState createState() => _WeekCalendarViewState();
}
class _WeekCalendarViewState extends State<WeekCalendarView> {
final PageController _pageController = PageController(initialPage: 500);
int _currentPage = 500;
// DateTime
late DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = DateTime.now().withoutTime; //
}
// page
List<DateTime> _getWeekDatesForPage(int page) {
final now = DateTime.now();
final baseSunday =
DateTime(now.year, now.month, now.day - (now.weekday % 7));
final daysOffset = (page - 500) * 7;
final targetSunday = baseSunday.add(Duration(days: daysOffset));
return List.generate(
7,
(i) => DateTime(
targetSunday.year, targetSunday.month, targetSunday.day + i));
}
//
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
//
bool _isSelected(DateTime date) {
return date.year == _selectedDate.year &&
date.month == _selectedDate.month &&
date.day == _selectedDate.day;
}
//
bool _hasData(DateTime date) {
return widget.hasData?.call(date.withoutTime) ?? false;
}
void _onDateSelected(DateTime date) {
setState(() {
_selectedDate = date.withoutTime;
});
//
widget.onDateSelected?.call(date);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildWeekRangeLabel(_currentPage),
SizedBox(height: 10.h),
SizedBox(
height: 100.h,
child: PageView.builder(
controller: _pageController,
itemCount: 1000,
itemBuilder: (context, page) {
final weekDates = _getWeekDatesForPage(page);
return Row(
children: weekDates.asMap().entries.map((entry) {
final int index = entry.key;
final DateTime date = entry.value;
final bool isSelected = _isSelected(date);
final bool hasData = _hasData(date);
final bool isToday = _isToday(date);
//
Color textColor;
if (isSelected) {
textColor = Colors.white; //
} else if (hasData) {
textColor = Colors.black; //
} else if (isToday) {
textColor = Colors.black; //
} else {
textColor = Colors.grey; //
}
//
Color? bgColor;
if (isSelected) {
bgColor = AppColors.mainColor; //
}
//
return GestureDetector(
onTap: () => _onDateSelected(date),
child: Container(
padding: EdgeInsets.all(4.w),
width: 75.w,
height: 75.w,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(50.r),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
[
'简写周日',
'简写周一',
'简写周二',
'简写周三',
'简写周四',
'简写周五',
'简写周六'
][index]
.tr,
style: TextStyle(
fontSize: 14.sp,
color: textColor,
fontWeight: FontWeight.w400,
),
),
Text(
date.day.toString(),
style: TextStyle(
fontSize: 26.sp,
color: textColor,
fontWeight: FontWeight.w600,
),
),
if (isToday && !isSelected) //
SizedBox(height: 2.h),
if (isToday && !isSelected)
Container(
width: 6.w,
height: 6.w,
decoration: BoxDecoration(
color: AppColors.mainColor,
shape: BoxShape.circle,
),
),
],
),
),
);
}).toList(),
);
},
onPageChanged: (page) {
setState(() {
_currentPage = page;
});
//
final dates = _getWeekDatesForPage(page);
final startOfWeek = dates.first;
final endOfWeek = dates.last;
//
widget.onWeekChanged?.call(startOfWeek, endOfWeek);
},
),
),
],
);
}
Widget _buildWeekRangeLabel(int page) {
final dates = _getWeekDatesForPage(page);
final start = dates[0];
final end = dates[6];
String label;
if (start.year == end.year) {
// "2025年8月18日 - 8月24日"
label =
'${start.year}${''.tr}${start.month}${''.tr}${start.day}${''.tr} - ${end.month}${''.tr}${end.day}${''.tr}';
} else {
// "2024年12月31日 - 2025年1月6日"
label =
'${start.year}${''.tr}${start.month}${''.tr}${start.day}${''.tr} - ${end.year}${''.tr}${end.month}${''.tr}${end.day}${''.tr}';
}
return Text(
label,
style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.w600),
);
}
}

View File

@ -253,10 +253,10 @@ class AddFingerprintLogic extends BaseGetXController {
final List<int> getTokenList = changeStringListToIntList(token!);
String startTime = DateTool().dateToHNString(state.effectiveDateTime.value);
String endTime = DateTool().dateToHNString(state.failureDateTime.value);
if (F.isSKY) {
startTime = '255:00';
endTime = '255:00';
}
// if (F.isSKY) {
// startTime = '255:00';
// endTime = '255:00';
// }
final String command = SenderAddFingerprintWithTimeCycleCoercionCommand(
keyID: '1',

View File

@ -89,27 +89,27 @@ class _CatEyeCustomModePageState extends State<CatEyeCustomModePage> {
SizedBox(
height: 30.h,
),
Container(
margin: EdgeInsets.only(left: 20.w),
child: CommonItem(
leftTitel: '实时画面'.tr,
rightTitle: state.realTimeMode.value,
isHaveLine: false,
isHaveDirection: true,
isHaveRightWidget: false,
action: () {
Navigator.pushNamed(context, Routers.liveVideoPage,
arguments: {
'lockSetInfoData': state.lockSetInfoData.value,
'catEyeConfigData': state.lockSetInfoData.value
.lockSettingInfo!.catEyeConfig!.isNotEmpty
? state.lockSetInfoData.value.lockSettingInfo!
.catEyeConfig![0]
: null
});
},
),
)
// Container(
// margin: EdgeInsets.only(left: 20.w),
// child: CommonItem(
// leftTitel: '实时画面'.tr,
// rightTitle: state.realTimeMode.value,
// isHaveLine: false,
// isHaveDirection: true,
// isHaveRightWidget: false,
// action: () {
// Navigator.pushNamed(context, Routers.liveVideoPage,
// arguments: {
// 'lockSetInfoData': state.lockSetInfoData.value,
// 'catEyeConfigData': state.lockSetInfoData.value
// .lockSettingInfo!.catEyeConfig!.isNotEmpty
// ? state.lockSetInfoData.value.lockSettingInfo!
// .catEyeConfig![0]
// : null
// });
// },
// ),
// )
],
),
),

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_setSupportFunctionsWithParameters.dart';
import 'package:star_lock/blue/io_reply.dart';
@ -82,30 +83,31 @@ class CatEyeSetLogic extends BaseGetXController {
//
cancelBlueConnetctToastTimer();
dismissEasyLoading();
AppLog.log('state.settingOptions.value:${state.settingOptions.value}');
switch (state.settingOptions.value) {
case 1: //
{
updateAutoLightScreenConfig();
await updateAutoLightScreenConfig();
}
break;
case 2: //
{
updateStayWarnConfig();
await updateStayWarnConfig();
}
break;
case 3: //
{
updateAbnormalWarnConfig();
await updateAbnormalWarnConfig();
}
break;
case 4: //
{
updateLightScreenTimeConfig();
await updateLightScreenTimeConfig();
}
break;
case 5: //
{
updateCatEyeModeConfig();
await updateCatEyeModeConfig();
}
break;
default:
@ -288,6 +290,7 @@ class CatEyeSetLogic extends BaseGetXController {
.catEyeConfig![0]
.catEyeModeConfig
?.realTimeMode = state.catEyeConfig.value.realTimeMode;
eventBus
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
}
@ -456,6 +459,10 @@ class CatEyeSetLogic extends BaseGetXController {
}
void sendBlueMessage() {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final message = _buildCatEyeSetBlueMessage();
BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState connectionState) async {

View File

@ -80,12 +80,12 @@ class _CatEyeSetPageState extends State<CatEyeSetPage> {
isHaveRightWidget: true,
rightWidget: _otherToDoSwitch(2),
)),
Obx(() => CommonItem(
leftTitel: '异常警告'.tr,
rightTitle: '',
isHaveLine: true,
isHaveRightWidget: true,
rightWidget: _otherToDoSwitch(3))),
// Obx(() => CommonItem(
// leftTitel: '异常警告'.tr,
// rightTitle: '',
// isHaveLine: true,
// isHaveRightWidget: true,
// rightWidget: _otherToDoSwitch(3))),
//ToDo
CommonItem(
leftTitel: '呼叫目标'.tr,

View File

@ -12,6 +12,8 @@ import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_otaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_processOtaUpgrade.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart';
@ -52,9 +54,14 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply);
} else if (reply is SetVoicePackageFinalResultReply) {
handleSetResult(reply);
} else if (reply is ReadLockCurrentVoicePacketReply) {
handleLockCurrentVoicePacketResult(reply);
}
});
await initList();
readLockLanguage();
}
///
@ -93,7 +100,7 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
final passthroughItem = PassthroughItem(
lang: element.lang,
timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr,
langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name,
);
state.languages.add(passthroughItem);
@ -432,12 +439,11 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
_handlerVoicePackageConfigureConfirmation(
VoicePackageConfigureConfirmationReply reply,
) async {
final int status = reply.data[2];
switch (status) {
case 0x00:
cancelBlueConnetctToastTimer();
final LoginEntity entity =
await ApiRepository.to.settingCurrentVoiceTimbre(
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final LoginEntity entity = await ApiRepository.to.settingCurrentVoiceTimbre(
data: {
'lang': state.tempLangStr.value,
'timbre': state.tempTimbreStr.value,
@ -445,15 +451,38 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
lockId: state.lockSetInfoData.value.lockId!,
);
if (entity.errorCode!.codeIsSuccessful) {
showSuccess('设置成功'.tr, something: () {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.lang = state.tempLangStr.value;
showSuccess('设置成功'.tr, something: () async {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre?.lang =
state.tempLangStr.value;
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.timbre = state.tempTimbreStr.value;
eventBus.fire(
PassCurrentLockInformationEvent(state.lockSetInfoData.value));
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
SetVoicePackageFinalResult(
lockID: BlueManage().connectDeviceName,
languageCode: state.tempLangStr.value,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
await Future.delayed(Duration(seconds: 1));
});
}
}
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
final int status = reply.data[2];
switch (status) {
case 0x00:
cancelBlueConnetctToastTimer();
dismissEasyLoading();
break;
default:
@ -461,4 +490,74 @@ class SpeechLanguageSettingsLogic extends BaseGetXController {
break;
}
}
void handleLockCurrentVoicePacketResult(
ReadLockCurrentVoicePacketReply reply) {
final int status = reply.data[2];
switch (status) {
case 0x00:
//
cancelBlueConnetctToastTimer();
const int languageCodeStartIndex = 3;
const int languageCodeLength = 20;
const int languageCodeEndIndex =
languageCodeStartIndex + languageCodeLength; // 23
if (reply.data.length < languageCodeEndIndex) {
throw Exception(
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
}
List<int> languageCodeBytes =
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
String languageCode = String.fromCharCodes(languageCodeBytes);
languageCode = languageCode.trim(); //
languageCode =
languageCode.replaceAll('\u0000', ''); // (null bytes)
if (languageCode != null && languageCode != '') {
final indexWhere = state.languages
.indexWhere((element) => element.lang == languageCode);
if (indexWhere != -1) {
print('锁板上的语言是:$languageCode,下标是:$indexWhere');
state.selectPassthroughListIndex.value = indexWhere;
}
}
dismissEasyLoading();
break;
case 0x06:
//
final List<int> token = reply.data.sublist(2, 6);
if (state.data != null) {
sendFileToDevice(state.data!, token);
}
break;
default:
break;
}
}
void readLockLanguage() async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
ReadLockCurrentVoicePacket(
lockID: BlueManage().connectDeviceName,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
}
}

View File

@ -63,6 +63,12 @@ class _SpeechLanguageSettingsPageState
final soundType = state.soundTypeList.value[index];
return CommonItem(
leftTitel: soundType,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight: state.selectSoundTypeIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '',
isHaveLine: !isLastItem,
isHaveDirection: false,
@ -94,7 +100,8 @@ class _SpeechLanguageSettingsPageState
height: 8.h,
),
//
Container(
Obx(
() => Container(
color: Colors.transparent,
child: Column(
children: List.generate(
@ -103,10 +110,18 @@ class _SpeechLanguageSettingsPageState
final item = state.languages[index];
return CommonItem(
leftTitel: item.langText,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight: state.selectPassthroughListIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '',
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
leftTitleMaxWidth: 0.9.sw,
//
rightWidget:
state.selectPassthroughListIndex.value == index
? Image(
@ -125,66 +140,6 @@ class _SpeechLanguageSettingsPageState
),
),
),
],
),
),
);
}
Widget _buildBody() {
return Obx(
() => SingleChildScrollView(
child: Column(
children: [
ListView.builder(
itemCount: state.soundTypeList.length,
itemBuilder: (BuildContext context, int index) {
// itemCount - 1
final isLastItem = index == state.soundTypeList.length - 1;
// platFormSet RxList<Platform>
final platform = state.soundTypeList.value[index];
return CommonItem(
leftTitel: state.soundTypeList.value[index],
rightTitle: '',
isHaveLine: !isLastItem,
// 线
isHaveDirection: false,
isHaveRightWidget: true,
rightWidget: Radio<String>(
// Radio 使 id
value: platform,
// selectPlatFormIndex id
groupValue: state.soundTypeList
.value[state.selectSoundTypeIndex.value],
//
activeColor: AppColors.mainColor,
// Radio
onChanged: (value) {
if (value != null) {
setState(() {
// id
final newIndex = state.soundTypeList.value
.indexWhere((p) => p == value);
if (newIndex != -1) {
state.selectSoundTypeIndex.value = newIndex;
}
});
}
},
),
action: () {
setState(() {
state.selectSoundTypeIndex.value = index;
});
},
);
},
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics() //add this line,
),
Column(
children: _buildList(),
),
],
),
@ -192,16 +147,7 @@ class _SpeechLanguageSettingsPageState
);
}
List<Widget> _buildList() {
final appLocalLanguages = state.languages;
return List.generate(
appLocalLanguages.length,
(index) => _buildItem(
appLocalLanguages[index],
index,
),
);
}
@override
void dispose() {
@ -211,24 +157,4 @@ class _SpeechLanguageSettingsPageState
}
}
_buildItem(PassthroughItem item, index) {
return CommonItem(
leftTitel: item.langText,
rightTitle: '',
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
rightWidget: state.selectPassthroughListIndex.value == index
? Image(
image: const AssetImage('images/icon_item_checked.png'),
width: 30.w,
height: 30.w,
fit: BoxFit.contain,
)
: Container(),
action: () {
state.selectPassthroughListIndex.value = index;
},
);
}
}

View File

@ -16,6 +16,7 @@ class ThirdPartyPlatformState {
final RxList<String> platFormSet = List.of({
'锁通通'.tr,
'涂鸦智能'.tr,
'Matter'.tr ,
}).obs;
RxInt selectPlatFormIndex = 0.obs;

View File

@ -145,9 +145,9 @@ class EditVideoLogLogic extends BaseGetXController {
}
// URL生成唯一的文件名MD5哈希值
String getFileNameFromUrl(String url, String extension) {
String getFileNameFromUrl(String url, String extension, int recordType) {
final hash = md5.convert(utf8.encode(url)).toString(); // 使 md5
return '$hash.$extension';
return '$recordType' + '_' + '$hash.$extension';
}
Future<void> recordDownloadTime(String filePath) async {
@ -169,7 +169,7 @@ class EditVideoLogLogic extends BaseGetXController {
}
//
Future<String?> downloadFile(String? url) async {
Future<String?> downloadFile(String? url, int recordType) async {
if (url == null || url.isEmpty) {
print('URL不能为空');
return null;
@ -183,7 +183,8 @@ class EditVideoLogLogic extends BaseGetXController {
// URL生成唯一文件名
String extension = _getFileTypeFromUrl(url); //
String fileName = getFileNameFromUrl(url, extension); // URL生成唯一文件名
String fileName =
getFileNameFromUrl(url, extension, recordType); // URL生成唯一文件名
String savePath = '${appDocDir.path}/downloads/$fileName'; //
//

View File

@ -76,23 +76,31 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
body: Column(
children: <Widget>[
Expanded(
child: Obx(() => ListView.builder(
child: Obx(
() => ListView.builder(
itemCount: state.videoLogList.length,
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item = state.videoLogList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
])),
mainListView(index, item)
],
return ExpansionTile(
shape: Border(),
collapsedShape: Border(),
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 400),
),
initiallyExpanded: true,
title: Text(
item.date ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
children: mainListView(index, item),
);
})),
},
),
),
),
bottomBottomBtnWidget()
],
@ -100,29 +108,35 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
);
}
Widget _buildNotData() {
return Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
'images/icon_noData.png',
width: 160.w,
height: 180.h,
),
Text(
'暂无数据'.tr,
style: TextStyle(
color: AppColors.darkGrayTextColor, fontSize: 22.sp),
)
],
),
),
);
}
double itemW = (1.sw - 15.w * 4) / 3;
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 10.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
//
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
// Widget videoItem(RecordListData recordData, int index) {
@ -237,9 +251,9 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
if (state.selectVideoLogList.value.isNotEmpty) {
state.selectVideoLogList.value.forEach((element) {
if (element.videoUrl != null && element.videoUrl != '') {
logic.downloadFile(element.videoUrl ?? '');
logic.downloadFile(element.videoUrl ?? '', element.recordType!);
} else if (element.imagesUrl != null && element.imagesUrl != '') {
logic.downloadFile(element.imagesUrl ?? '');
logic.downloadFile(element.imagesUrl ?? '', element.recordType!);
}
});
// double _progress = 0.0;
@ -338,55 +352,6 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
Widget videoItem(RecordListData recordData) {
return GestureDetector(
onTap: () {
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
Get.toNamed(Routers.videoLogDetailPage, arguments: <String, Object>{
'recordData': recordData,
'videoDataList': state.videoLogList.value
});
} else if (recordData.imagesUrl != null &&
recordData.imagesUrl!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FullScreenImagePage(
imageUrl: recordData.imagesUrl!,
),
),
);
}
},
child: Stack(
children: [
SizedBox(
width: itemW,
height: itemH,
child: Column(
children: <Widget>[
Container(
width: itemW,
height: itemW,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
borderRadius: BorderRadius.circular(10.w),
child: _buildImageOrVideoItem(recordData),
),
),
SizedBox(height: 5.h),
Text(
DateTool()
.dateToYMDHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.sp),
)
],
),
),
Positioned(
top: 0.w,
right: 0.w,
child: GestureDetector(
onTap: () {
recordData.isSelect = !recordData.isSelect!;
if (recordData.isSelect! == true) {
@ -396,20 +361,146 @@ class _EditVideoLogPageState extends State<EditVideoLogPage> {
}
setState(() {});
},
child: Image(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Image(
width: 36.w,
height: 36.w,
image: state.selectVideoLogList.value.contains(recordData)
? const AssetImage('images/icon_round_select.png')
: const AssetImage('images/icon_round_unSelect.png'),
),
SizedBox(
width: 14.w,
),
Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(58.w),
color: AppColors.mainColor,
),
child: Icon(
_buildIconByType(recordData),
size: 48.sp,
color: Colors.white,
),
),
SizedBox(
width: 14.w,
),
Container(
height: itemW,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_buildTitleByType(recordData),
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(
height: 8.h,
),
Text(
DateTool()
.dateToHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
),
),
)
],
),
),
],
),
Container(
width: 118.w,
height: 118.w,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
borderRadius: BorderRadius.circular(10.w),
child: _buildImageOrVideoItem(recordData),
),
),
],
),
),
);
}
String _buildTitleByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return '防拆报警'.tr;
case 160:
return '人脸'.tr + '开锁'.tr;
case 220:
return '逗留警告'.tr;
default:
return '';
}
}
IconData _buildIconByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return Icons.fmd_bad_outlined;
case 160:
return Icons.tag_faces_outlined;
case 220:
return Icons.wifi_tethering_error_rounded_outlined;
default:
return Icons.priority_high_rounded;
}
}
Color _buildTextColorByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 120:
case 150:
case 130:
case 190:
case 200:
case 210:
case 220:
return Colors.red;
default:
return Colors.black;
}
}
_buildImageOrVideoItem(RecordListData recordData) {
if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) {
return _buildVideoItem(recordData);

View File

@ -1,7 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:star_lock/appRouters.dart';
@ -63,8 +63,17 @@ class VideoLogLogic extends BaseGetXController {
final content = await File(logFilePath).readAsString();
final logData = Map<String, int>.from(json.decode(content));
//
logData.forEach((filePath, timestamp) {
String fileName = filePath
.split('/')
.last; // : 220_f5e371111918ff70cb3532bec20e38c4.mp4
String withoutExt = fileName.replaceAll('.mp4', ''); // 使 substring
String numberStr = withoutExt.split('_').first; // : 220
int number = int.parse(numberStr);
print(number); // : 220
final downloadDateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
final dateKey =
'${downloadDateTime.year}-${downloadDateTime.month.toString().padLeft(2, '0')}-${downloadDateTime.day.toString().padLeft(2, '0')}';
@ -77,11 +86,15 @@ class VideoLogLogic extends BaseGetXController {
//
if (filePath.endsWith('.jpg')) {
groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, imagesUrl: filePath),
RecordListData(
operateDate: timestamp,
imagesUrl: filePath,
recordType: number),
);
} else if (filePath.endsWith('.mp4')) {
groupedDownloads[dateKey]?.add(
RecordListData(operateDate: timestamp, videoUrl: filePath),
RecordListData(
operateDate: timestamp, videoUrl: filePath, recordType: number),
);
}
});

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/flavors.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_state.dart';
@ -26,9 +27,7 @@ class VideoLogPage extends StatefulWidget {
class _VideoLogPageState extends State<VideoLogPage> {
final VideoLogLogic logic = Get.put(VideoLogLogic());
final VideoLogState state = Get
.find<VideoLogLogic>()
.state;
final VideoLogState state = Get.find<VideoLogLogic>().state;
@override
void initState() {
@ -56,8 +55,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
// title加编辑按钮
editVideoTip(),
Obx(
() =>
Visibility(
() => Visibility(
visible: !state.isNavLocal.value,
child: state.videoLogList.length > 0
? Expanded(
@ -66,17 +64,22 @@ class _VideoLogPageState extends State<VideoLogPage> {
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item =
state.videoLogList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
])),
mainListView(index, item)
],
return ExpansionTile(
shape: Border(),
collapsedShape: Border(),
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 400),
),
initiallyExpanded: true,
title: Text(
item.date ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
children: mainListView(index, item),
);
},
),
@ -86,8 +89,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
),
//
Obx(
() =>
Visibility(
() => Visibility(
visible: state.isNavLocal.value,
child: state.lockVideoList.length > 0
? Expanded(
@ -96,20 +98,22 @@ class _VideoLogPageState extends State<VideoLogPage> {
itemBuilder: (BuildContext c, int index) {
final CloudStorageData item =
state.lockVideoList[index];
return Column(
children: <Widget>[
Container(
margin: EdgeInsets.only(
left: 20.w, top: 15.w, bottom: 15.w),
child: Row(
children: <Widget>[
Text(item.date ?? '',
style: TextStyle(fontSize: 20.sp)),
],
return ExpansionTile(
shape: Border(),
collapsedShape: Border(),
expansionAnimationStyle: AnimationStyle(
curve: Curves.easeInOut,
duration: Duration(milliseconds: 400),
),
initiallyExpanded: true,
title: Text(
item.date ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
lockMainListView(index, item)
],
children: mainListView(index, item),
);
},
),
@ -161,8 +165,9 @@ class _VideoLogPageState extends State<VideoLogPage> {
// logic.clearDownloads();
});
},
child: Obx(() =>
Text('云存'.tr,
child: Obx(
() => Text(
'云存'.tr,
style: state.isNavLocal.value == true
? TextStyle(
color: Colors.grey,
@ -171,7 +176,10 @@ class _VideoLogPageState extends State<VideoLogPage> {
: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600)))),
fontWeight: FontWeight.w600),
),
),
),
TextButton(
onPressed: () {
setState(() {
@ -180,8 +188,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
});
},
child: Obx(
() =>
Text(
() => Text(
'已下载'.tr,
style: state.isNavLocal.value == true
? TextStyle(
@ -215,10 +222,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
EdgeInsets.only(left: 20.w, top: 20.w, bottom: 20.w, right: 10.w),
decoration: BoxDecoration(
color: const Color(0xFFF6F7F8),
borderRadius: BorderRadius.circular(20.h)),
borderRadius: BorderRadius.circular(
20.h,
),
),
child: Obx(
() =>
Column(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
@ -227,15 +236,11 @@ class _VideoLogPageState extends State<VideoLogPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('3天滚动储存'.tr,
style: TextStyle(fontSize: 24.sp)),
Text('3天滚动储存'.tr, style: TextStyle(fontSize: 24.sp)),
SizedBox(height: 10.h),
Text("${F
.navTitle}${"已为本设备免费提供3大滚动视频储存服务"
.tr}",
Text("${F.navTitle}${"已为本设备免费提供3大滚动视频储存服务".tr}",
style:
TextStyle(fontSize: 22.sp, color: Colors
.grey)),
TextStyle(fontSize: 22.sp, color: Colors.grey)),
],
)),
SizedBox(width: 15.w),
@ -243,8 +248,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
Image(
width: 40.w,
height: 24.w,
image: const AssetImage(
'images/icon_right_black.png'))
image: const AssetImage('images/icon_right_black.png'))
],
),
SizedBox(
@ -263,8 +267,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
visible: state.validityPeriodInfo.value != null &&
state.validityPeriodInfo.value?.status == 1,
child: Text(
'过期时间:${state.validityPeriodInfo.value
?.validityPeriodEnd}',
'过期时间:${state.validityPeriodInfo.value?.validityPeriodEnd}',
style: TextStyle(
fontSize: 24.sp,
),
@ -290,8 +293,7 @@ class _VideoLogPageState extends State<VideoLogPage> {
visible: state.validityPeriodInfo.value != null &&
state.validityPeriodInfo.value?.status == 1,
child: Text(
'剩余天数:${state.validityPeriodInfo.value
?.remainingDays} ',
'剩余天数:${state.validityPeriodInfo.value?.remainingDays}',
style: TextStyle(
fontSize: 24.sp,
),
@ -412,48 +414,12 @@ class _VideoLogPageState extends State<VideoLogPage> {
double itemH = (1.sw - 15.w * 4) / 3 + 40.h;
//
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 15.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
Widget lockMainListView(int index, CloudStorageData itemData) {
return GridView.builder(
padding: EdgeInsets.only(left: 15.w, right: 15.w),
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
//
mainAxisSpacing: 15.w,
//
crossAxisSpacing: 15.w,
//
childAspectRatio: itemW / itemH),
itemBuilder: (BuildContext context, int index) {
final RecordListData recordData = itemData.recordList![index];
return videoItem(recordData);
},
);
List<Widget> lockMainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
Widget videoItem(RecordListData recordData) {
@ -469,22 +435,86 @@ class _VideoLogPageState extends State<VideoLogPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FullScreenImagePage(
builder: (context) => FullScreenImagePage(
imageUrl: recordData.imagesUrl!,
),
),
);
}
},
child: SizedBox(
width: itemW,
height: itemH,
child: Column(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(58.w),
color: AppColors.mainColor,
),
child: Icon(
_buildIconByType(recordData),
size: 48.sp,
color: Colors.white,
),
),
SizedBox(
width: 14.w,
),
Container(
width: itemW,
height: itemW,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_buildTitleByType(recordData),
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(
height: 8.h,
),
Text(
DateTool()
.dateToHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
Container(
width: 118.w,
height: 118.w,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
@ -492,12 +522,6 @@ class _VideoLogPageState extends State<VideoLogPage> {
child: _buildImageOrVideoItem(recordData),
),
),
SizedBox(height: 5.h),
Text(
DateTool().dateToYMDHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18.sp),
)
],
),
),
@ -512,6 +536,50 @@ class _VideoLogPageState extends State<VideoLogPage> {
}
}
String _buildTitleByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return '防拆报警'.tr;
case 160:
return '人脸'.tr + '开锁'.tr;
case 220:
return '逗留警告'.tr;
default:
return '';
}
}
IconData _buildIconByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return Icons.fmd_bad_outlined;
case 160:
return Icons.tag_faces_outlined;
case 220:
return Icons.wifi_tethering_error_rounded_outlined;
default:
return Icons.priority_high_rounded;
}
}
Color _buildTextColorByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 120:
case 150:
case 130:
case 190:
case 200:
case 210:
case 220:
return Colors.red;
default:
return Colors.black;
}
}
_buildVideoItem(RecordListData recordData) {
return VideoThumbnailImage(videoUrl: recordData.videoUrl!);
}

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLog/videoLog_entity.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/controlsOverlay_page.dart';
import 'package:star_lock/main/lockDetail/videoLog/videoLogDetail/videoLogDetail_state.dart';
@ -30,11 +34,11 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
@override
void initState() {
super.initState();
AppLog.log(
'state.recordData.value.videoUrl!' + state.recordData.value.videoUrl!);
state.videoController = VideoPlayerController.networkUrl(
Uri.parse(state.recordData.value.videoUrl!),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
state.videoController =
createVideoController(state.recordData.value.videoUrl!);
state.videoController.addListener(() {
setState(() {});
@ -47,10 +51,8 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
if (state.videoController != null) {
await state.videoController.dispose(); //
}
state.videoController = VideoPlayerController.networkUrl(
Uri.parse(videoUrl),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
state.videoController =
createVideoController(state.recordData.value.videoUrl!);
//
await state.videoController.initialize();
@ -60,6 +62,22 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
setState(() {});
}
VideoPlayerController createVideoController(String url) {
if (url.startsWith('http://') || url.startsWith('https://')) {
return VideoPlayerController.networkUrl(
Uri.parse(url),
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
} else {
final file = File(
url.startsWith('file://') ? url.replaceFirst('file://', '') : url);
return VideoPlayerController.file(
file,
videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -104,6 +122,7 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
],
),
),
// _buildTitleRow(),
_buildOther(),
],
)
@ -135,14 +154,79 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
);
}
},
child: SizedBox(
width: itemW,
height: itemH,
child: Column(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20.w),
margin: EdgeInsets.only(
bottom: 20.h,
left: 18.w,
right: 18.w,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, 3), // changes position of shadow
),
],
),
width: 1.sw,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Row(
children: [
Container(
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(58.w),
color: AppColors.mainColor,
),
child: Icon(
_buildIconByType(recordData),
size: 48.sp,
color: Colors.white,
),
),
SizedBox(
width: 14.w,
),
Container(
width: itemW,
height: itemW,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_buildTitleByType(recordData),
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(
height: 8.h,
),
Text(
DateTool()
.dateToHNString(recordData.operateDate.toString()),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
Container(
width: 118.w,
height: 118.w,
margin: const EdgeInsets.all(0),
color: Colors.white,
child: ClipRRect(
@ -213,11 +297,13 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
margin: EdgeInsets.only(left: 20.w, top: 15.w, bottom: 15.w),
child: Row(
children: <Widget>[
Text(item.date ?? '', style: TextStyle(fontSize: 20.sp)),
Text(item.date ?? '',
style: TextStyle(
fontSize: 24.sp, fontWeight: FontWeight.w600)),
],
),
),
mainListView(index, item),
...mainListView(index, item),
],
);
},
@ -225,22 +311,60 @@ class _VideoLogDetailPageState extends State<VideoLogDetailPage> {
);
}
Widget mainListView(int index, CloudStorageData itemData) {
return GridView.builder(
itemCount: itemData.recordList!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//
crossAxisCount: 3,
),
itemBuilder: (BuildContext context, int index) {
return _buildItem(itemData.recordList![index]);
},
);
//
List<Widget> mainListView(int index, CloudStorageData itemData) {
return itemData.recordList!.map((e) => videoItem(e)).toList();
}
String _buildTitleByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return '防拆报警'.tr;
case 160:
return '人脸'.tr + '开锁'.tr;
case 220:
return '逗留警告'.tr;
default:
return '';
}
}
IconData _buildIconByType(RecordListData item) {
final recordType = item.recordType;
switch (recordType) {
case 130:
return Icons.fmd_bad_outlined;
case 160:
return Icons.tag_faces_outlined;
case 220:
return Icons.wifi_tethering_error_rounded_outlined;
default:
return Icons.priority_high_rounded;
}
}
_buildItem(itemData) {
return videoItem(itemData);
}
_buildTitleRow() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
),
padding: EdgeInsets.only(left: 15.w, top: 24.w, bottom: 24.w),
child: Row(
children: [
Text(
_buildTitleByType(state.recordData.value) ?? '',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}

View File

@ -1,53 +1,48 @@
import 'dart:io'; // dart:io 使 File
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:path_provider/path_provider.dart'; // path_provider
import 'package:video_thumbnail/video_thumbnail.dart'; // video_thumbnail
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
class VideoThumbnailImage extends StatefulWidget {
final String videoUrl;
VideoThumbnailImage({required this.videoUrl});
const VideoThumbnailImage({Key? key, required this.videoUrl})
: super(key: key);
@override
_VideoThumbnailState createState() => _VideoThumbnailState();
}
class _VideoThumbnailState extends State<VideoThumbnailImage> {
final Map<String, String> _thumbnailCache = {}; //
late Future<String?> _thumbnailFuture; // Future
// 使 static
static final Map<String, Future<String?>> _pendingThumbnails = {};
late Future<String?> _thumbnailFuture;
@override
void initState() {
super.initState();
_thumbnailFuture = _generateThumbnail(); // initState Future
// URL Future
_thumbnailFuture = _pendingThumbnails.putIfAbsent(widget.videoUrl, () {
return _generateThumbnail(widget.videoUrl);
});
}
//
Future<String?> _generateThumbnail() async {
// per URL
Future<String?> _generateThumbnail(String url) async {
try {
//
if (_thumbnailCache.containsKey(widget.videoUrl)) {
return _thumbnailCache[widget.videoUrl];
}
//
final tempDir = await getTemporaryDirectory();
final thumbnailPath = await VideoThumbnail.thumbnailFile(
video: widget.videoUrl,
// URL
final thumbnail = await VideoThumbnail.thumbnailFile(
video: url,
thumbnailPath: tempDir.path,
//
imageFormat: ImageFormat.JPEG,
//
maxHeight: 200,
//
quality: 100, // (0-100)
quality: 100,
);
//
_thumbnailCache[widget.videoUrl] = thumbnailPath!;
return thumbnailPath;
return thumbnail;
} catch (e) {
print('Failed to generate thumbnail: $e');
return null;
@ -57,27 +52,25 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
@override
Widget build(BuildContext context) {
return FutureBuilder<String?>(
future: _thumbnailFuture, // Future
future: _thumbnailFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
//
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError || !snapshot.hasData) {
//
return Image.asset(
'images/icon_unHaveData.png', //
return Center(
child: Image.asset(
'images/icon_unHaveData.png',
fit: BoxFit.cover,
),
);
} else {
//
final thumbnailPath = snapshot.data!;
return Stack(
alignment: Alignment.center,
children: <Widget>[
RotatedBox(
quarterTurns: -1,
child: Image.file(
File(thumbnailPath), //
File(snapshot.data!),
width: 200,
height: 200,
fit: BoxFit.cover,
@ -85,7 +78,7 @@ class _VideoThumbnailState extends State<VideoThumbnailImage> {
),
Icon(
Icons.play_arrow_rounded,
size: 80,
size: 88.sp,
color: Colors.white.withOpacity(0.8),
),
],

View File

@ -297,6 +297,11 @@ class LockListInfoItemEntity {
LockListInfoItemEntity copy() {
return LockListInfoItemEntity.fromJson(toJson());
}
@override
String toString() {
return 'LockListInfoItemEntity{keyId: $keyId, lockId: $lockId, lockName: $lockName, lockAlias: $lockAlias, electricQuantity: $electricQuantity, fwVersion: $fwVersion, hwVersion: $hwVersion, keyType: $keyType, passageMode: $passageMode, userType: $userType, startDate: $startDate, endDate: $endDate, weekDays: $weekDays, remoteEnable: $remoteEnable, faceAuthentication: $faceAuthentication, lastFaceValidateTime: $lastFaceValidateTime, nextFaceValidateTime: $nextFaceValidateTime, keyRight: $keyRight, keyStatus: $keyStatus, isLockOwner: $isLockOwner, sendDate: $sendDate, lockUserNo: $lockUserNo, senderUserId: $senderUserId, electricQuantityDate: $electricQuantityDate, electricQuantityStandby: $electricQuantityStandby, isOnlyManageSelf: $isOnlyManageSelf, restoreCount: $restoreCount, model: $model, vendor: $vendor, bluetooth: $bluetooth, lockFeature: $lockFeature, lockSetting: $lockSetting, hasGateway: $hasGateway, appUnlockOnline: $appUnlockOnline, mac: $mac, initUserNo: $initUserNo, updateDate: $updateDate, network: $network}';
}
}
class NetworkInfo {
@ -323,6 +328,11 @@ class NetworkInfo {
data['isOnline'] = isOnline;
return data;
}
@override
String toString() {
return 'NetworkInfo{peerId: $peerId, wifiName: $wifiName, isOnline: $isOnline}';
}
}
class Bluetooth {
@ -356,6 +366,11 @@ class Bluetooth {
data['signKey'] = signKey;
return data;
}
@override
String toString() {
return 'Bluetooth{bluetoothDeviceId: $bluetoothDeviceId, bluetoothDeviceName: $bluetoothDeviceName, publicKey: $publicKey, privateKey: $privateKey, signKey: $signKey}';
}
}
class LockFeature {
@ -442,6 +457,11 @@ class LockFeature {
data['isMJpeg'] = isMJpeg;
return data;
}
@override
String toString() {
return 'LockFeature{password: $password, passwordIssue: $passwordIssue, icCard: $icCard, fingerprint: $fingerprint, fingerVein: $fingerVein, palmVein: $palmVein, isSupportIris: $isSupportIris, d3Face: $d3Face, bluetoothRemoteControl: $bluetoothRemoteControl, videoIntercom: $videoIntercom, isSupportCatEye: $isSupportCatEye, isSupportBackupBattery: $isSupportBackupBattery, isNoSupportedBlueBroadcast: $isNoSupportedBlueBroadcast, wifiLockType: $wifiLockType, wifi: $wifi, isH264: $isH264, isH265: $isH265, isMJpeg: $isMJpeg}';
}
}
class LockSetting {
@ -486,6 +506,11 @@ class LockSetting {
}
return data;
}
@override
String toString() {
return 'LockSetting{attendance: $attendance, appUnlockOnline: $appUnlockOnline, remoteUnlock: $remoteUnlock, catEyeConfig: $catEyeConfig}';
}
}
// CatEyeConfig

View File

@ -11,6 +11,8 @@ import 'package:star_lock/appRouters.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/blue/blue_manage.dart';
import 'package:star_lock/blue/io_protocol/io_getDeviceModel.dart';
import 'package:star_lock/blue/io_protocol/io_readVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_setVoicePackageFinalResult.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigure.dart';
import 'package:star_lock/blue/io_protocol/io_voicePackageConfigureProcess.dart';
import 'package:star_lock/blue/io_reply.dart';
@ -50,9 +52,14 @@ class LockVoiceSettingLogic extends BaseGetXController {
_handlerVoicePackageConfigureProcess(reply);
} else if (reply is VoicePackageConfigureConfirmationReply) {
handleVoiceConfigureThrottled(reply);
} else if (reply is ReadLockCurrentVoicePacketReply) {
handleLockCurrentVoicePacketResult(reply);
} else if (reply is SetVoicePackageFinalResultReply) {
handleSetResult(reply);
}
});
initList();
readLockLanguage();
}
void handleVoiceConfigureThrottled(
@ -73,6 +80,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
Future<void> _executeLogic(
VoicePackageConfigureConfirmationReply reply) async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
final LoginEntity entity = await ApiRepository.to.settingCurrentVoiceTimbre(
data: {
'lang': state.tempLangStr.value,
@ -81,18 +92,47 @@ class LockVoiceSettingLogic extends BaseGetXController {
lockId: state.lockSetInfoData.value.lockId!,
);
if (entity.errorCode!.codeIsSuccessful) {
showSuccess('设置成功'.tr, something: () {
showSuccess('设置成功'.tr, something: () async {
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre?.lang =
state.tempLangStr.value;
state.lockSetInfoData.value.lockSettingInfo?.currentVoiceTimbre
?.timbre = state.tempTimbreStr.value;
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
SetVoicePackageFinalResult(
lockID: BlueManage().connectDeviceName,
languageCode: state.tempLangStr.value,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
await Future.delayed(Duration(seconds: 1));
eventBus
.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value));
Get.offAllNamed(Routers.starLockMain);
});
}
}
void handleSetResult(SetVoicePackageFinalResultReply reply) async {
final int status = reply.data[2];
switch (status) {
case 0x00:
cancelBlueConnetctToastTimer();
dismissEasyLoading();
break;
default:
showToast('设置'.tr + '失败'.tr);
break;
}
}
void saveSpeechLanguageSettings() async {
@ -202,9 +242,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
case 0x00:
//
cancelBlueConnetctToastTimer();
_startSendLanguageFile();
break;
case 0x06:
//
@ -214,7 +252,8 @@ class LockVoiceSettingLogic extends BaseGetXController {
}
break;
default:
showToast('获取设备型号失败'.tr);
dismissEasyLoading();
cancelBlueConnetctToastTimer();
break;
}
}
@ -250,11 +289,10 @@ class LockVoiceSettingLogic extends BaseGetXController {
if (lang == 'zh_TW') {
//
List<String> parts = lang.split('_');
final indexOf = locales.indexOf(Locale(parts[0], parts[1]));
final passthroughItem = PassthroughItem(
lang: element.lang,
timbres: element.timbres,
langText: '简体中文'.tr + '(中国台湾)'.tr,
langText: '简体中文'.tr + '(中国台湾)'.tr + 'Simplified Chinese TW',
name: element.name,
);
state.languages.add(passthroughItem);
@ -268,6 +306,7 @@ class LockVoiceSettingLogic extends BaseGetXController {
ExtensionLanguageType.fromLocale(locales[indexOf]).lanTitle,
name: element.name,
);
state.languages.add(passthroughItem);
}
});
@ -403,4 +442,77 @@ class LockVoiceSettingLogic extends BaseGetXController {
state.data = null;
super.onClose();
}
void readLockLanguage() async {
showEasyLoading();
showBlueConnetctToastTimer(action: () {
dismissEasyLoading();
});
await BlueManage().blueSendData(BlueManage().connectDeviceName,
(BluetoothConnectionState deviceConnectionState) async {
if (deviceConnectionState == BluetoothConnectionState.connected) {
await BlueManage().writeCharacteristicWithResponse(
ReadLockCurrentVoicePacket(
lockID: BlueManage().connectDeviceName,
).packageData(),
);
} else if (deviceConnectionState ==
BluetoothConnectionState.disconnected) {
dismissEasyLoading();
cancelBlueConnetctToastTimer();
showBlueConnetctToast();
}
});
}
void handleLockCurrentVoicePacketResult(
ReadLockCurrentVoicePacketReply reply) {
final int status = reply.data[2];
switch (status) {
case 0x00:
//
cancelBlueConnetctToastTimer();
const int languageCodeStartIndex = 3;
const int languageCodeLength = 20;
const int languageCodeEndIndex =
languageCodeStartIndex + languageCodeLength; // 23
if (reply.data.length < languageCodeEndIndex) {
throw Exception(
'Reply data is too short to contain LanguageCode. Expected at least $languageCodeEndIndex bytes, got ${reply.data.length}');
}
List<int> languageCodeBytes =
reply.data.sublist(languageCodeStartIndex, languageCodeEndIndex);
String languageCode = String.fromCharCodes(languageCodeBytes);
languageCode = languageCode.trim(); //
languageCode =
languageCode.replaceAll('\u0000', ''); // (null bytes)
print('LanguageCode: $languageCode'); // : zh_CN, en_US
if (languageCode != null && languageCode != '') {
final indexWhere = state.languages
.indexWhere((element) => element.lang == languageCode);
if (indexWhere != -1) {
print('锁板上的语言是:$languageCode,下标是:$indexWhere');
state.selectPassthroughListIndex.value = indexWhere;
}
}
dismissEasyLoading();
break;
case 0x06:
//
final List<int> token = reply.data.sublist(2, 6);
if (state.data != null) {
sendFileToDevice(state.data!, token);
}
break;
default:
break;
}
}
}

View File

@ -95,6 +95,12 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
final soundType = state.soundTypeList.value[index];
return CommonItem(
leftTitel: soundType,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight: state.selectSoundTypeIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '',
isHaveLine: !isLastItem,
isHaveDirection: false,
@ -135,10 +141,18 @@ class _LockVoiceSettingState extends State<LockVoiceSetting> {
final item = state.languages[index];
return CommonItem(
leftTitel: item.langText,
leftTitleStyle: TextStyle(
fontSize: 20.sp,
fontWeight:
state.selectPassthroughListIndex.value == index
? FontWeight.bold
: null,
),
rightTitle: '',
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
leftTitleMaxWidth: 0.85.sw,
rightWidget:
state.selectPassthroughListIndex.value == index
? Image(

View File

@ -8,7 +8,7 @@ import 'messageDetail_state.dart';
class MessageDetailLogic extends BaseGetXController {
final MessageDetailState state = MessageDetailState();
//
//
Future<void> readMessageDataRequest() async {
final MessageListEntity entity = await ApiRepository.to.readMessageLoadData(messageId:state.itemData.value.id!);
if (entity.errorCode!.codeIsSuccessful) {

View File

@ -24,14 +24,22 @@ class MessageListEntity {
}
return data;
}
@override
String toString() {
return 'MessageListEntity{errorCode: $errorCode, description: $description, errorMsg: $errorMsg, data: $data}';
}
}
class Data {
List<MessageItemEntity>? list;
int? pageNo;
int? pageSize;
int? total;
int? readCount;
int? unreadCount;
Data({this.list, this.pageNo, this.pageSize});
Data({this.list, this.pageNo, this.pageSize, this.total,this.readCount, this.unreadCount});
Data.fromJson(Map<String, dynamic> json) {
if (json['list'] != null) {
@ -42,6 +50,9 @@ class Data {
}
pageNo = json['pageNo'];
pageSize = json['pageSize'];
total = json['total'];
readCount = json['readCount'];
unreadCount = json['unreadCount'];
}
Map<String, dynamic> toJson() {
@ -51,8 +62,16 @@ class Data {
}
data['pageNo'] = pageNo;
data['pageSize'] = pageSize;
data['total'] = total;
data['readCount'] = readCount;
data['unreadCount'] = unreadCount;
return data;
}
@override
String toString() {
return 'Data{list: $list, pageNo: $pageNo, pageSize: $pageSize, total: $total, readCount: $readCount, unreadCount: $unreadCount}';
}
}
class MessageItemEntity {
@ -78,4 +97,9 @@ class MessageItemEntity {
data['readAt'] = readAt;
return data;
}
@override
String toString() {
return 'MessageItemEntity{id: $id, data: $data, createdAt: $createdAt, readAt: $readAt}';
}
}

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/tools/baseGetXController.dart';
import '../../../network/api_repository.dart';
import '../../../tools/eventBusEventManage.dart';
@ -18,6 +19,10 @@ class MessageListLogic extends BaseGetXController {
final MessageListEntity entity = await ApiRepository.to
.messageListLoadData(pageNo: pageNo.toString(), pageSize: pageSize);
if (entity.errorCode!.codeIsSuccessful) {
AppLog.log('消息列表数据请求成功:${entity.data!.total}');
//
await FlutterAppBadger.updateBadgeCount(entity.data!.unreadCount!);
if (pageNo == 1) {
state.itemDataList.value = entity.data!.list!;
pageNo++;

View File

@ -104,6 +104,7 @@ class _MineMultiLanguagePageState extends State<MineMultiLanguagePage> {
isHaveLine: true,
isHaveDirection: false,
isHaveRightWidget: true,
leftTitleMaxWidth: 0.9.sw, //
rightWidget: state.currentLanguageType.value == lanType
? Image(
image: const AssetImage('images/icon_item_checked.png'),

View File

@ -2169,7 +2169,8 @@ class ApiProvider extends BaseProvider {
readMessageURL.toUrl,
jsonEncode({
'id': messageId,
}));
}),
isUnShowLoading: true);
//
Future<Response> deletMessageLoadData(String messageId) => post(

View File

@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/talk/starChart/constant/message_type_constant.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
@ -15,6 +16,7 @@ import 'package:star_lock/talk/starChart/proto/generic.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/eventBusEventManage.dart';
import 'package:star_lock/tools/storage.dart';
import '../../star_chart_manage.dart';
@ -43,6 +45,25 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle
//
talkeRequestOverTimeTimerManager.renew();
talkeRequestOverTimeTimerManager.cancel();
//
//
AppLog.log('msg:${scpMessage}');
AppLog.log('msg:${startChartManage.lockListPeerId}');
// id
final fromPeerId = scpMessage.FromPeerId;
if (fromPeerId != null && fromPeerId != '') {
startChartManage.lockListPeerId.forEach((element) {
if (element != null &&
element.network != null &&
element.network!.peerId == fromPeerId) {
//
eventBus.fire(ReadTalkMessageRefreshUI(element.lockName!));
}
});
}
// rbcuInfo数据
// startChartManage.startSendingRbcuInfoMessages(
// ToPeerId: startChartManage.lockPeerId);

View File

@ -1,11 +1,45 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:star_lock/mine/message/messageList/messageList_entity.dart';
import 'package:star_lock/network/api_repository.dart';
import 'package:star_lock/network/start_chart_api.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
import 'package:star_lock/tools/baseGetXController.dart';
import 'package:star_lock/tools/eventBusEventManage.dart';
import 'package:star_lock/tools/storage.dart';
class AppLifecycleObserver extends WidgetsBindingObserver {
//
StreamSubscription? _readMessageRefreshUIEvent;
void _readMessageRefreshUIAction() {
// eventBus
_readMessageRefreshUIEvent =
eventBus.on<ReadTalkMessageRefreshUI>().listen((event) async {
//
final MessageListEntity entity = await ApiRepository.to
.messageListLoadData(pageNo: '1', pageSize: '1');
if (entity.errorCode!.codeIsSuccessful) {
final lockName = event.lockName;
if (lockName != null && lockName.isNotEmpty) {
final readAt = entity.data?.list?.first.readAt == 0;
final data = entity.data?.list?.first.data;
if (readAt && data != null && data.contains(lockName)) {
//
final entity2 = await ApiRepository.to
.readMessageLoadData(messageId: entity.data!.list!.first.id!);
if (entity2.errorCode!.codeIsSuccessful) {
eventBus.fire(ReadMessageRefreshUI());
}
}
}
}
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
@ -37,6 +71,7 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
Get.back();
}
StartChartManage().destruction();
_readMessageRefreshUIEvent?.cancel();
}
void onAppResumed() async {
@ -52,6 +87,9 @@ class AppLifecycleObserver extends WidgetsBindingObserver {
StartChartApi.to.startChartHost =
loginData!.starchart!.scdUrl ?? StartChartApi.to.startChartHost;
}
//
_readMessageRefreshUIAction();
print('App has resumed to the foreground.');
}

View File

@ -110,22 +110,8 @@ class ImageTransmissionLogic extends BaseGetXController {
//
switch (contentType) {
case TalkData_ContentTypeE.G711:
// //
if (_isFirstAudioFrame) {
_startAudioTime = currentTime;
_isFirstAudioFrame = false;
}
//
final expectedTime = _startAudioTime + talkData.durationMs;
final audioDelay = currentTime - expectedTime;
//
if (audioDelay > 500) {
state.audioBuffer.clear();
if (state.isOpenVoice.value) {
_playAudioFrames();
}
//
if (!state.isOpenVoice.value && state.isRecordingAudio.value) {
return;
}
if (state.audioBuffer.length >= audioBufferSize) {
@ -212,7 +198,8 @@ class ImageTransmissionLogic extends BaseGetXController {
///
void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value) {
if (state.isOpenVoice.value &&
state.isRecordingAudio.value == false) {
final list =
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
// // PCM PcmArrayInt16
@ -561,6 +548,8 @@ class ImageTransmissionLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
@ -580,7 +569,6 @@ class ImageTransmissionLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -600,47 +588,65 @@ class ImageTransmissionLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
// 使
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// chunkSize
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
//
_bufferedAudioFrames.removeRange(0, chunkSize);
//
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
//
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null &&
_bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer =
Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
@ -649,27 +655,11 @@ class ImageTransmissionLogic extends BaseGetXController {
AppLog.log(error.message!);
}
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0);
for (int i = 0; i < pcmData.length; i++) {
// PCM数据通常是有符号的16位整数
int sample = pcmData[i];
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
return pcmData.map((sample) {
//
int amplified = (sample * gainFactor).round();
return amplified.clamp(-32768, 32767);
}).toList();
}
}

View File

@ -9,7 +9,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
@ -18,28 +17,20 @@ import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/login/login/entity/LoginEntity.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/network/api_repository.dart';
import 'package:star_lock/talk/call/callTalk.dart';
import 'package:star_lock/talk/call/g711.dart';
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
import 'package:star_lock/tools/G711Tool.dart';
import 'package:star_lock/tools/bugly/bugly_tool.dart';
import 'package:star_lock/tools/callkit_handler.dart';
import 'package:star_lock/tools/commonDataManage.dart';
import 'package:star_lock/tools/storage.dart';
import 'package:video_decode_plugin/nalu_utils.dart';
import 'package:video_decode_plugin/video_decode_plugin.dart';
import '../../../../tools/baseGetXController.dart';
@ -51,7 +42,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
int bufferSize = 25; //
int audioBufferSize = 2; // 2
int audioBufferSize = 20; // 2
// frameSeq较小时阈值也小
int _getFrameSeqRolloverThreshold(int lastSeq) {
@ -107,12 +98,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.isLoading.value = true;
//
final config = VideoDecoderConfig(
width: 864,
width: StartChartManage().videoWidth,
//
height: 480,
height: StartChartManage().videoHeight,
codecType: 'h264',
);
// textureId
AppLog.log(
'StartChartManage().videoWidth:${StartChartManage().videoWidth}');
AppLog.log(
'StartChartManage().videoHeight:${StartChartManage().videoHeight}');
final textureId = await VideoDecodePlugin.initDecoder(config);
if (textureId != null) {
Future.microtask(() => state.textureId.value = textureId);
@ -144,11 +139,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
FlutterPcmSound.setLogLevel(LogLevel.none);
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
// feed
if (Platform.isAndroid) {
FlutterPcmSound.setFeedThreshold(1024); // Android
} else {
FlutterPcmSound.setFeedThreshold(2000); // Android
}
// if (Platform.isAndroid) {
// FlutterPcmSound.setFeedThreshold(1024); // Android
// } else {
// FlutterPcmSound.setFeedThreshold(4096); // Android
// }
}
///
@ -498,16 +493,21 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value && state.isLoading.isFalse) {
final list =
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
// // PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
if (state.isOpenVoice.value &&
state.isLoading.isFalse &&
state.isRecordingAudio.value == false) {
List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0A-law
// PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
FlutterPcmSound.feed(fromList);
if (!state.isPlaying.value) {
AppLog.log('play');
FlutterPcmSound.play();
state.isPlaying.value = true;
}
} else if (state.isOpenVoice.isFalse) {
FlutterPcmSound.pause();
state.isPlaying.value = false;
}
}
@ -573,8 +573,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
void onInit() {
super.onInit();
//
_startListenTalkData();
//
_startListenTalkStatus();
//
@ -596,6 +594,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// H264帧缓冲区
state.h264FrameBuffer.clear();
state.isProcessingFrame = false;
//
_startListenTalkData();
}
@override
@ -639,7 +640,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// I帧集合
_decodedIFrames.clear();
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
super.onClose();
}
@ -652,33 +655,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void updateTalkExpect() {
// VideoTypeE的映射
final Map<String, VideoTypeE> qualityToVideoType = {
'标清': VideoTypeE.H264,
'高清': VideoTypeE.H264_720P,
//
};
TalkExpectReq talkExpectReq = TalkExpectReq();
state.isOpenVoice.value = !state.isOpenVoice.value;
// videoType
VideoTypeE currentVideoType =
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
if (!state.isOpenVoice.value) {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [],
);
showToast('已静音'.tr);
if (state.isOpenVoice.isTrue) {
FlutterPcmSound.play();
} else {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [AudioTypeE.G711],
);
FlutterPcmSound.pause();
}
///
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
talkExpect: talkExpectReq);
}
///
@ -762,6 +744,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
@ -781,7 +765,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -801,47 +784,74 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
return pcmData.map((sample) {
//
int amplified = (sample * gainFactor).round();
return amplified.clamp(-32768, 32767);
}).toList();
}
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 35; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
// 使
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// chunkSize
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
//
_bufferedAudioFrames.removeRange(0, chunkSize);
//
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
//
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null &&
_bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer =
Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
@ -850,476 +860,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
AppLog.log(error.message!);
}
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0);
for (int i = 0; i < pcmData.length; i++) {
// PCM数据通常是有符号的16位整数
int sample = pcmData[i];
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
}
/// h264文件frameType
Future<void> _appendH264FrameToFile(
List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
try {
if (state.h264File == null) {
await _initH264File();
}
// NALU分割函数NALU的完整字节数组
List<List<int>> splitNalus(List<int> data) {
List<List<int>> nalus = [];
int i = 0;
while (i < data.length - 3) {
int start = -1;
int next = -1;
if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
}
// I帧前只缓存SPS/PPS/IDR
if (!_hasWrittenFirstIFrame) {
final nalus = splitNalus(frameData);
List<List<int>> spsList = [];
List<List<int>> ppsList = [];
List<List<int>> idrList = [];
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 7) {
spsList.add(nalu);
// AppLog.log('SPS内容: ' +
// nalu
// .map((b) => b.toRadixString(16).padLeft(2, '0'))
// .join(' '));
} else if (naluType == 8) {
ppsList.add(nalu);
// AppLog.log('PPS内容: ' +
// nalu
// .map((b) => b.toRadixString(16).padLeft(2, '0'))
// .join(' '));
} else if (naluType == 5) {
idrList.add(nalu);
}
//
}
}
// I帧写入前缓存
if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) {
for (final sps in spsList) {
await _writeSingleFrameToFile(_ensureStartCode(sps));
// AppLog.log('写入顺序: SPS');
}
for (final pps in ppsList) {
await _writeSingleFrameToFile(_ensureStartCode(pps));
// AppLog.log('写入顺序: PPS');
}
for (final idr in idrList) {
await _writeSingleFrameToFile(_ensureStartCode(idr));
// AppLog.log('写入顺序: IDR');
}
_hasWrittenFirstIFrame = true;
} else {
// SPS/PPS/IDR则继续缓存I帧
if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList);
if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList);
if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList);
}
} else {
// IDR和P帧
final nalus = splitNalus(frameData);
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 5) {
await _writeSingleFrameToFile(_ensureStartCode(nalu));
// AppLog.log('写入顺序: IDR');
} else if (naluType == 1) {
await _writeSingleFrameToFile(_ensureStartCode(nalu));
// AppLog.log('写入顺序: P帧');
} else if (naluType == 7) {
// AppLog.log('遇到新SPS已忽略');
} else if (naluType == 8) {
// AppLog.log('遇到新PPS已忽略');
}
//
}
}
}
} catch (e) {
AppLog.log('写入H264帧到文件失败: $e');
}
}
// NALU起始码为0x00000001
List<int> _ensureStartCode(List<int> nalu) {
if (nalu.length >= 4 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x00 &&
nalu[3] == 0x01) {
return nalu;
} else if (nalu.length >= 3 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x01) {
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
} else {
return [0x00, 0x00, 0x00, 0x01] + nalu;
}
}
/// NALU头判断
Future<void> _writeSingleFrameToFile(List<int> frameData) async {
bool hasNaluHeader = false;
if (frameData.length >= 4 &&
frameData[0] == 0x00 &&
frameData[1] == 0x00 &&
((frameData[2] == 0x01) ||
(frameData[2] == 0x00 && frameData[3] == 0x01))) {
hasNaluHeader = true;
}
if (hasNaluHeader) {
await state.h264File!.writeAsBytes(frameData, mode: FileMode.append);
} else {
final List<int> naluHeader = [0x00, 0x00, 0x01];
await state.h264File!
.writeAsBytes(naluHeader + frameData, mode: FileMode.append);
}
}
/// h264文件
Future<void> _initH264File() async {
try {
if (state.h264File != null) return;
// Download目录
Directory? downloadsDir;
if (Platform.isAndroid) {
// Android 10+ getExternalStorageDirectory()
downloadsDir = await getExternalStorageDirectory();
// ROMDownload
final downloadPath = '/storage/emulated/0/Download';
if (Directory(downloadPath).existsSync()) {
downloadsDir = Directory(downloadPath);
}
} else {
downloadsDir = await getApplicationDocumentsDirectory();
}
final filePath =
'${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264';
state.h264FilePath = filePath;
state.h264File = File(filePath);
if (!await state.h264File!.exists()) {
await state.h264File!.create(recursive: true);
}
AppLog.log('H264文件初始化: $filePath');
} catch (e) {
AppLog.log('H264文件初始化失败: $e');
}
}
/// h264文件
Future<void> _closeH264File() async {
try {
if (state.h264File != null) {
AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}');
}
state.h264File = null;
state.h264FilePath = null;
_preIFrameCache.clear();
_hasWrittenFirstIFrame = false;
} catch (e) {
AppLog.log('关闭H264文件时出错: $e');
}
}
/// I帧数据中分割NALU并将SPS/PPS优先放入缓冲区
void _extractAndBufferSpsPpsForBuffer(
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
List<List<int>> splitNalus(List<int> data) {
List<List<int>> nalus = [];
int i = 0;
while (i < data.length - 3) {
int start = -1;
int next = -1;
if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
}
final nalus = splitNalus(frameData);
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 7) {
// SPS
hasSps = true;
//
if (spsCache == null || !_listEquals(spsCache!, nalu)) {
spsCache = List<int>.from(nalu);
}
} else if (naluType == 8) {
// PPS
hasPps = true;
if (ppsCache == null || !_listEquals(ppsCache!, nalu)) {
ppsCache = List<int>.from(nalu);
}
}
}
}
}
// List比较工具
bool _listEquals(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// I帧处理方法
// void _handleIFrameWithSpsPpsAndIdr(
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
// // I帧前所有未处理帧SPS/PPS/I帧
// state.h264FrameBuffer.clear();
// _extractAndBufferSpsPpsForBuffer(
// frameData, durationMs, frameSeq, frameSeqI);
// // SPS/PPS就先写入I帧本体IDR
// if (spsCache == null || ppsCache == null) {
// // SPS/PPS缓存I帧
// return;
// }
// // SPS/PPS
// _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// // I帧包IDRtype 5
// List<List<int>> nalus = [];
// int i = 0;
// List<int> data = frameData;
// while (i < data.length - 3) {
// int start = -1;
// int next = -1;
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
// if (data[i + 2] == 0x01) {
// start = i;
// i += 3;
// } else if (i + 3 < data.length &&
// data[i + 2] == 0x00 &&
// data[i + 3] == 0x01) {
// start = i;
// i += 4;
// } else {
// i++;
// continue;
// }
// next = i;
// while (next < data.length - 3) {
// if (data[next] == 0x00 &&
// data[next + 1] == 0x00 &&
// ((data[next + 2] == 0x01) ||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
// break;
// }
// next++;
// }
// nalus.add(data.sublist(start, next));
// i = next;
// } else {
// i++;
// }
// }
// int nalusTotalLen =
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
// if (nalus.isEmpty && data.isNotEmpty) {
// nalus.add(data);
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
// nalus.add(data.sublist(nalusTotalLen));
// }
// for (final nalu in nalus) {
// int offset = 0;
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
// if (nalu[2] == 0x01)
// offset = 3;
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
// }
// if (nalu.length > offset) {
// int naluType = nalu[offset] & 0x1F;
// if (naluType == 5) {
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs,
// frameSeq, frameSeqI);
// }
// }
// }
// }
// P帧处理方法
// void _handlePFrame(
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
// // P帧type 1
// List<List<int>> nalus = [];
// int i = 0;
// List<int> data = frameData;
// while (i < data.length - 3) {
// int start = -1;
// int next = -1;
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
// if (data[i + 2] == 0x01) {
// start = i;
// i += 3;
// } else if (i + 3 < data.length &&
// data[i + 2] == 0x00 &&
// data[i + 3] == 0x01) {
// start = i;
// i += 4;
// } else {
// i++;
// continue;
// }
// next = i;
// while (next < data.length - 3) {
// if (data[next] == 0x00 &&
// data[next + 1] == 0x00 &&
// ((data[next + 2] == 0x01) ||
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
// break;
// }
// next++;
// }
// nalus.add(data.sublist(start, next));
// i = next;
// } else {
// i++;
// }
// }
// int nalusTotalLen =
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
// if (nalus.isEmpty && data.isNotEmpty) {
// nalus.add(data);
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
// nalus.add(data.sublist(nalusTotalLen));
// }
// for (final nalu in nalus) {
// int offset = 0;
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
// if (nalu[2] == 0x01)
// offset = 3;
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
// }
// if (nalu.length > offset) {
// int naluType = nalu[offset] & 0x1F;
// if (naluType == 1) {
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs,
// frameSeq, frameSeqI);
// }
// }
// }
// }
//
void onQualityChanged(String quality) async {
state.currentQuality.value = quality;
@ -1432,6 +972,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
//
switch (contentType) {
case TalkData_ContentTypeE.G711:
//
if (!state.isOpenVoice.value || state.isRecordingAudio.value) {
return;
}
if (state.audioBuffer.length >= audioBufferSize) {
state.audioBuffer.removeAt(0); //
}

View File

@ -109,22 +109,10 @@ class TalkViewLogic extends BaseGetXController {
//
switch (contentType) {
case TalkData_ContentTypeE.G711:
// //
if (_isFirstAudioFrame) {
_startAudioTime = currentTime;
_isFirstAudioFrame = false;
}
//
final expectedTime = _startAudioTime + talkData.durationMs;
final audioDelay = currentTime - expectedTime;
//
if (audioDelay > 500) {
state.audioBuffer.clear();
if (state.isOpenVoice.value) {
_playAudioFrames();
}
//
if (!state.isOpenVoice.value || state.isRecordingAudio.value) {
print(
'录音时丢弃数据:${state.isOpenVoice.value}-${state.isRecordingAudio.value}');
return;
}
if (state.audioBuffer.length >= audioBufferSize) {
@ -388,9 +376,11 @@ class TalkViewLogic extends BaseGetXController {
if (state.videoBuffer.isNotEmpty) {
final TalkData oldestFrame = state.videoBuffer.removeAt(0);
if (oldestFrame.content.isNotEmpty) {
state.listData.value = Uint8List.fromList(oldestFrame.content); //
state.listData.value =
Uint8List.fromList(oldestFrame.content); //
final int decodeStart = DateTime.now().millisecondsSinceEpoch;
decodeImageFromList(Uint8List.fromList(oldestFrame.content)).then((ui.Image img) {
decodeImageFromList(Uint8List.fromList(oldestFrame.content))
.then((ui.Image img) {
final int decodeEnd = DateTime.now().millisecondsSinceEpoch;
state.currentImage.value = img;
_renderedFrameCount++;
@ -558,6 +548,8 @@ class TalkViewLogic extends BaseGetXController {
state.voiceProcessor = VoiceProcessor.instance;
}
Timer? _startProcessingAudioTimer;
//
Future<void> startProcessingAudio() async {
try {
@ -577,7 +569,6 @@ class TalkViewLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -597,47 +588,65 @@ class TalkViewLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally {
//
if (_startProcessingAudioTimer != null) {
// 53200
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
}
//
Future<void> _onFrame(List<int> frame) async {
//
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); //
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
//
return;
}
//
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
// 使
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// chunkSize
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
//
_bufferedAudioFrames.removeRange(0, chunkSize);
//
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
//
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); //
}
} else {
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null &&
_bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer =
Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
@ -648,25 +657,10 @@ class TalkViewLogic extends BaseGetXController {
//
List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0);
for (int i = 0; i < pcmData.length; i++) {
// PCM数据通常是有符号的16位整数
int sample = pcmData[i];
//
double amplified = sample * gainFactor;
//
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
return pcmData.map((sample) {
//
int amplified = (sample * gainFactor).round();
return amplified.clamp(-32768, 32767);
}).toList();
}
}

View File

@ -21,6 +21,7 @@ class CommonItem extends StatelessWidget {
this.isTipsImg,
this.action,
this.leftTitleMaxWidth, //
this.leftTitleStyle, //
this.tipsImgAction})
: super(key: key);
String? leftTitel;
@ -35,6 +36,7 @@ class CommonItem extends StatelessWidget {
bool? setHeight;
bool? isTipsImg;
bool? isPadding;
TextStyle? leftTitleStyle; //
final double? leftTitleMaxWidth; //
@override
@ -65,7 +67,7 @@ class CommonItem extends StatelessWidget {
),
child: Text(
leftTitel!,
style: TextStyle(fontSize: 22.sp),
style: leftTitleStyle ?? TextStyle(fontSize: 22.sp),
overflow: TextOverflow.ellipsis, //
maxLines: 3, // 2
),

View File

@ -131,6 +131,13 @@ class ReadMessageRefreshUI {
ReadMessageRefreshUI();
}
///
class ReadTalkMessageRefreshUI {
ReadTalkMessageRefreshUI(this.lockName);
String lockName;
}
///
class ElectronicKeyListRefreshUI {
ElectronicKeyListRefreshUI();

View File

@ -206,130 +206,130 @@ extension ExtensionLanguageType on LanguageType {
var str = '';
switch (this) {
case LanguageType.english:
str = '英文'.tr;
str = '英文'.tr + 'English';
break;
case LanguageType.chinese:
str = '简体中文'.tr;
str = '简体中文'.tr + 'Simplified Chinese';
break;
case LanguageType.traditionalChineseTW:
str = '繁体中文(中国台湾)'.tr;
str = '繁体中文(中国台湾)'.tr + 'Traditional Chinese TW';
break;
case LanguageType.traditionalChineseHK:
str = '繁体中文(中国香港)'.tr;
str = '繁体中文(中国香港)'.tr + 'Traditional Chinese HK';
break;
case LanguageType.french:
str = '法语'.tr;
str = '法语'.tr + 'French';
break;
case LanguageType.russian:
str = '俄语'.tr;
str = '俄语'.tr + 'Russian';
break;
case LanguageType.german:
str = '德语'.tr;
str = '德语'.tr + 'German';
break;
case LanguageType.japanese:
str = '日语'.tr;
str = '日语'.tr + 'Japanese';
break;
case LanguageType.korean:
str = '韩语'.tr;
str = '韩语'.tr + 'Korean';
break;
case LanguageType.italian:
str = '意大利语'.tr;
str = '意大利语'.tr + 'Italian';
break;
case LanguageType.portuguese:
str = '葡萄牙语'.tr;
str = '葡萄牙语'.tr + 'Portuguese';
break;
case LanguageType.spanish:
str = '西班牙语'.tr;
str = '西班牙语'.tr + 'Spanish';
break;
case LanguageType.arabic:
str = '阿拉伯语'.tr;
str = '阿拉伯语'.tr + 'Arabic';
break;
case LanguageType.vietnamese:
str = '越南语'.tr;
str = '越南语'.tr + 'Vietnamese';
break;
case LanguageType.malay:
str = '马来语'.tr;
str = '马来语'.tr + 'Malay';
break;
case LanguageType.dutch:
str = '荷兰语'.tr;
str = '荷兰语'.tr + 'Dutch';
break;
case LanguageType.romanian:
str = '罗马尼亚语'.tr;
str = '罗马尼亚语'.tr + 'Romanian';
break;
case LanguageType.lithuanian:
str = '立陶宛语'.tr;
str = '立陶宛语'.tr + 'Lithuanian';
break;
case LanguageType.swedish:
str = '瑞典语'.tr;
str = '瑞典语'.tr + 'Swedish';
break;
case LanguageType.estonian:
str = '爱沙尼亚语'.tr;
str = '爱沙尼亚语'.tr + 'Estonian';
break;
case LanguageType.polish:
str = '波兰语'.tr;
str = '波兰语'.tr + 'Polish';
break;
case LanguageType.slovak:
str = '斯洛伐克语'.tr;
str = '斯洛伐克语'.tr + 'Slovak';
break;
case LanguageType.czech:
str = '捷克语'.tr;
str = '捷克语'.tr + 'Czech';
break;
case LanguageType.greek:
str = '希腊语'.tr;
str = '希腊语'.tr + 'Greek';
break;
case LanguageType.hebrew:
str = '希伯来语'.tr;
str = '希伯来语'.tr + 'Hebrew';
break;
case LanguageType.serbian:
str = '塞尔维亚语'.tr;
str = '塞尔维亚语'.tr + 'Serbian';
break;
case LanguageType.turkish:
str = '土耳其语'.tr;
str = '土耳其语'.tr + 'Turkish';
break;
case LanguageType.hungarian:
str = '匈牙利语'.tr;
str = '匈牙利语'.tr + 'Hungarian';
break;
case LanguageType.bulgarian:
str = '保加利亚语'.tr;
str = '保加利亚语'.tr + 'Bulgarian';
break;
case LanguageType.kazakh:
str = '哈萨克斯坦语'.tr;
str = '哈萨克斯坦语'.tr + 'Kazakh';
break;
case LanguageType.bengali:
str = '孟加拉语'.tr;
str = '孟加拉语'.tr + 'Bengali';
break;
case LanguageType.croatian:
str = '克罗地亚语'.tr;
str = '克罗地亚语'.tr + 'Croatian';
break;
case LanguageType.thai:
str = '泰语'.tr;
str = '泰语'.tr + 'Thai';
break;
case LanguageType.indonesian:
str = '印度尼西亚语'.tr;
str = '印度尼西亚语'.tr + 'Indonesian';
break;
case LanguageType.finnish:
str = '芬兰语'.tr;
str = '芬兰语'.tr + 'Finnish';
break;
case LanguageType.danish:
str = '丹麦语'.tr;
str = '丹麦语'.tr + 'Danish';
break;
case LanguageType.ukrainian:
str = '乌克兰语'.tr;
str = '乌克兰语'.tr + 'Ukrainian';
break;
case LanguageType.hindi:
str = '印地语'.tr;
str = '印地语'.tr + 'Hindi';
break;
case LanguageType.urdu:
str = '乌尔都语'.tr;
str = '乌尔都语'.tr + 'Urdu';
break;
case LanguageType.armenian:
str = '亚美尼亚语'.tr;
str = '亚美尼亚语'.tr + 'Armenian';
break;
case LanguageType.georgian:
str = '格鲁吉亚语'.tr;
str = '格鲁吉亚语'.tr + 'Georgian';
break;
case LanguageType.brazilianPortuguese:
str = '巴西葡萄牙语'.tr;
str = '巴西葡萄牙语'.tr + 'Brazilian Portuguese';
break;
}
return str;