last63

home_page_full.dart

Nov 16th, 2025
44
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.30 KB | None | 0 0
  1. import 'dart:async'; // timer countdown
  2. import 'package:flutter/material.dart';
  3. import 'package:carousel_slider/carousel_slider.dart'; // carousel slider
  4. import 'package:http/http.dart' as http; // ambil data API JSON
  5. import 'dart:convert'; // decode JSON
  6. import 'package:geolocator/geolocator.dart'; // GPS
  7. import 'package:geocoding/geocoding.dart'; // Konversi GPS
  8. import 'package:intl/intl.dart'; // Formatter Number
  9. import 'package:permission_handler/permission_handler.dart'; // Izin handler
  10. import 'package:shared_preferences/shared_preferences.dart'; // cache lokal
  11. import 'package:string_similarity/string_similarity.dart'; // fuzzy match string
  12.  
  13. class HomePage extends StatefulWidget {
  14. const HomePage({super.key});
  15.  
  16. @override
  17. State<HomePage> createState() => _HomePageState();
  18. }
  19.  
  20. class _HomePageState extends State<HomePage> {
  21. final CarouselController _controller = CarouselController();
  22. int _currentIndex = 0;
  23. bool _isLoading = true;
  24. Duration? _timeRemaining;
  25. Timer? _countdownTimer;
  26. String _location = "Mengambil lokasi....";
  27. String _prayerTime = "Loading...";
  28. String _prayerName = "Loading...";
  29. String _backgroundImage = 'assets/images/bg_morning.png';
  30. List<dynamic>? _jadwalSholat;
  31.  
  32. final posterList = const <String>[
  33. 'assets/images/ramadhan-kareem.png',
  34. 'assets/images/idl-fitr.png',
  35. 'assets/images/idl-adh.png',
  36. ];
  37.  
  38. //fungsi teks remaining waktu sholat
  39. String _formatDuration(Duration d) {
  40. final hours = d.inHours;
  41. final minute = d.inMinutes.remainder(60);
  42. return "$hours jam $minute menit lagi";
  43. }
  44.  
  45. // state untuk dijalankan diawal
  46. @override
  47. void initState() {
  48. super.initState();
  49. _updatePrayerTimes();
  50. }
  51.  
  52. // ================================================================
  53. // LOGIKA INTI: UPDATE DATA WAKTU SHOLAT & LOKASI
  54. // ================================================================
  55. /// ๐Ÿ”น Mengambil lokasi, mendeteksi kota terdekat, dan memuat jadwal sholat
  56. ///
  57. /// Diagram alur:
  58. ///
  59. /// [GPS Position]
  60. /// [Kota terdekat (fuzzy match)]
  61. /// [Ambil data GitHub jadwal sholat]
  62. /// [Hitung waktu sholat terdekat + countdown]
  63.  
  64. Future<void> _updatePrayerTimes() async {
  65. setState(() => _isLoading = true);
  66.  
  67. if (await _requestLocationPermission()) {
  68. try {
  69. // Ambil posisi GPS user (timeout 10 detik)
  70. Position position =
  71. await Geolocator.getCurrentPosition(
  72. desiredAccuracy: LocationAccuracy.high,
  73. ).timeout(
  74. const Duration(seconds: 10),
  75. onTimeout: () =>
  76. throw Exception("Gagal mendapatkan lokasi (timeout)"),
  77. );
  78.  
  79. // Cari kota terdekat dari koordinat
  80. String city;
  81. try {
  82. city = await getClosestCity(position.latitude, position.longitude);
  83. } catch (_) {
  84. city = "semarang"; // fallback default
  85. }
  86.  
  87. // Konversi koordinat ke nama lokasi manusiawi
  88. List<Placemark> placemarks = await placemarkFromCoordinates(
  89. position.latitude,
  90. position.longitude,
  91. );
  92. Placemark place = placemarks.isNotEmpty
  93. ? placemarks.first
  94. : Placemark();
  95.  
  96. // Ambil bulan & tahun sekarang
  97. String month = DateFormat('MM').format(DateTime.now());
  98. String year = DateFormat('yyyy').format(DateTime.now());
  99.  
  100. // Ambil jadwal sholat & hitung waktu terdekat
  101. _jadwalSholat = await fetchJadwalSholat(city, month, year);
  102. _calculateNextPrayer();
  103.  
  104. // Update tampilan UI
  105. setState(() {
  106. _location =
  107. "${place.subAdministrativeArea ?? ''}, ${place.locality ?? ''}";
  108. _backgroundImage = _getBackgroundImage(DateTime.now());
  109. _isLoading = false;
  110. });
  111. } catch (e) {
  112. _showErrorDialog("Gagal mengambil data: ${e.toString()}");
  113. setState(() => _isLoading = false);
  114. }
  115. } else {
  116. _showErrorDialog(
  117. "Izin lokasi ditolak. Aktifkan lokasi untuk melanjutkan.",
  118. );
  119. setState(() => _isLoading = false);
  120. }
  121. }
  122.  
  123. // ================================================================
  124. // API CALL: AMBIL DATA JADWAL SHOLAT DARI GITHUB
  125. // ================================================================
  126. /// ๐Ÿ”น Mengambil data JSON jadwal sholat berdasarkan kota, bulan, dan tahun.
  127. /// ๐Ÿ”น Data disimpan ke cache agar bisa diakses offline.
  128. Future<List<dynamic>> fetchJadwalSholat(
  129. String city,
  130. String month,
  131. String year,
  132. ) async {
  133. final prefs = await SharedPreferences.getInstance();
  134. final cacheKey = "$city-$year-$month";
  135.  
  136. // Gunakan cache jika tersedia
  137. final cachedData = prefs.getString(cacheKey);
  138. if (cachedData != null) {
  139. return json.decode(cachedData) as List<dynamic>;
  140. }
  141.  
  142. // Fetch dari GitHub repo jadwalsholatorg
  143. final url =
  144. 'https://raw.githubusercontent.com/lakuapik/jadwalsholatorg/master/adzan/$city/$year/$month.json';
  145. final response = await http.get(Uri.parse(url));
  146.  
  147. if (response.statusCode == 200) {
  148. await prefs.setString(cacheKey, response.body);
  149. return json.decode(response.body) as List<dynamic>;
  150. }
  151.  
  152. // Fallback ke data tahun 2019 jika gagal
  153. final fallbackUrl =
  154. 'https://raw.githubusercontent.com/lakuapik/jadwalsholatorg/master/adzan/$city/2019/$month.json';
  155. final fallbackResponse = await http.get(Uri.parse(fallbackUrl));
  156.  
  157. if (fallbackResponse.statusCode == 200) {
  158. await prefs.setString(cacheKey, fallbackResponse.body);
  159. return json.decode(fallbackResponse.body) as List<dynamic>;
  160. }
  161.  
  162. throw Exception('Gagal memuat jadwal sholat untuk $city ($month-$year)');
  163. }
  164.  
  165. // ================================================================
  166. // LOGIKA FUZZY MATCH: DETEKSI KOTA TERDEKAT
  167. // ================================================================
  168. /// ๐Ÿ”น Mencocokkan nama kota pengguna dengan daftar kota dari GitHub.
  169. ///
  170. /// ๐Ÿ”ธ Contoh:
  171. /// "Jakarta Selatan" โ†’ dicocokkan โ†’ "jakarta"
  172. Future<String> getClosestCity(double userLat, double userLon) async {
  173. final response = await http.get(
  174. Uri.parse(
  175. 'https://raw.githubusercontent.com/lakuapik/jadwalsholatorg/master/kota.json',
  176. ),
  177. );
  178. if (response.statusCode != 200) throw Exception('Gagal memuat daftar kota');
  179.  
  180. final List<dynamic> cityList = json.decode(response.body);
  181. List<Placemark> placemarks = await placemarkFromCoordinates(
  182. userLat,
  183. userLon,
  184. );
  185. Placemark place = placemarks.first;
  186.  
  187. String userCity =
  188. (place.subAdministrativeArea ??
  189. place.locality ??
  190. place.administrativeArea ??
  191. "")
  192. .toLowerCase()
  193. .replaceAll(" ", "");
  194.  
  195. double bestScore = 0.0;
  196. String bestMatch = cityList.first;
  197. for (var city in cityList) {
  198. double score = city.toString().similarityTo(userCity);
  199. if (score > bestScore) {
  200. bestScore = score;
  201. bestMatch = city.toString();
  202. }
  203. }
  204. return bestMatch;
  205. }
  206.  
  207. // ================================================================
  208. // PERHITUNGAN: WAKTU SHOLAT BERIKUTNYA & COUNTDOWN
  209. // ================================================================
  210. /// ๐Ÿ”น Menghitung waktu sholat berikutnya berdasarkan jadwal hari ini.
  211. /// ๐Ÿ”น Menampilkan countdown waktu tersisa.
  212. void _calculateNextPrayer() {
  213. if (_jadwalSholat == null || _jadwalSholat!.isEmpty) return;
  214.  
  215. final now = DateTime.now();
  216. final format = DateFormat('HH:mm');
  217. final todayDate = DateFormat('yyyy-MM-dd').format(now);
  218.  
  219. var todaySchedule = _jadwalSholat?.firstWhere(
  220. (e) => e['tanggal'] == todayDate,
  221. orElse: () => null,
  222. );
  223. if (todaySchedule == null) return;
  224.  
  225. // Helper untuk parsing waktu
  226. DateTime parseTime(String hhmm) {
  227. final t = format.parse(hhmm);
  228. return DateTime(now.year, now.month, now.day, t.hour, t.minute);
  229. }
  230.  
  231. // Peta jadwal sholat
  232. final prayers = {
  233. "Shubuh": parseTime(todaySchedule['shubuh']),
  234. "Dzuhur": parseTime(todaySchedule['dzuhur']),
  235. "Ashar": parseTime(todaySchedule['ashr']),
  236. "Maghrib": parseTime(todaySchedule['magrib']),
  237. "Isya": parseTime(todaySchedule['isya']),
  238. };
  239.  
  240. // Cari waktu sholat berikutnya
  241. String nextPrayer = "Shubuh";
  242. Duration? closest;
  243. prayers.forEach((name, time) {
  244. final diff = time.difference(now);
  245. if (diff > Duration.zero && (closest == null || diff < closest!)) {
  246. closest = diff;
  247. nextPrayer = name;
  248. }
  249. });
  250.  
  251. setState(() {
  252. _prayerName = nextPrayer;
  253. _timeRemaining = closest;
  254. _prayerTime = closest != null
  255. ? DateFormat('HH:mm').format(prayers[nextPrayer]!)
  256. : "N/A";
  257. });
  258.  
  259. // Timer countdown
  260. _countdownTimer?.cancel();
  261. _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
  262. final remaining = prayers[nextPrayer]!.difference(DateTime.now());
  263. if (remaining.isNegative) {
  264. timer.cancel();
  265. _calculateNextPrayer();
  266. } else {
  267. setState(() => _timeRemaining = remaining);
  268. }
  269. });
  270. }
  271.  
  272. // ================================================================
  273. // PERMINTAAN IZIN AKSES LOKASI
  274. // ================================================================
  275. /// ๐Ÿ”น Meminta izin lokasi dari pengguna.
  276. Future<bool> _requestLocationPermission() async {
  277. var status = await Permission.location.status;
  278. if (status.isGranted) return true;
  279. if (status.isDenied) {
  280. status = await Permission.location.request();
  281. return status.isGranted;
  282. }
  283. if (status.isPermanentlyDenied) {
  284. await openAppSettings();
  285. }
  286. return false;
  287. }
  288.  
  289. // ================================================================
  290. // DIALOG ERROR
  291. // ================================================================
  292. /// ๐Ÿ”น Menampilkan dialog jika terjadi kesalahan (mis. gagal lokasi).
  293. void _showErrorDialog(String msg) {
  294. showDialog(
  295. context: context,
  296. builder: (_) => AlertDialog(
  297. title: const Text("Terjadi Kesalahan"),
  298. content: Text(msg),
  299. actions: [
  300. TextButton(
  301. onPressed: () => Navigator.pop(context),
  302. child: const Text("OK"),
  303. ),
  304. ],
  305. ),
  306. );
  307. }
  308.  
  309. String _getBackgroundImage(DateTime now) {
  310. if (now.hour < 12) {
  311. return 'assets/images/bg_morning.png';
  312. } else if (now.hour < 18) {
  313. return 'assets/images/bg_afternoon.png';
  314. }
  315.  
  316. return 'assets/images/bg_night.png';
  317. }
  318.  
  319. @override
  320. Widget build(BuildContext context) {
  321. return Scaffold(
  322. body: SafeArea(
  323. child: SingleChildScrollView(
  324. child: Column(
  325. children: <Widget>[
  326. // =============================
  327. // [MENU WAKTU SHOLAT BY LOKASI]
  328. // =============================
  329. _buildHeroSection(),
  330. const SizedBox(height: 65),
  331. // =============================
  332. // [MENU SECTION]
  333. // =============================
  334. _buildMenuGridSection(),
  335.  
  336. // =============================
  337. // [CAROUSEL SECTION]
  338. // =============================
  339. _buildCarouselSection(),
  340. ],
  341. ),
  342. ),
  343. ),
  344. );
  345. }
  346.  
  347. // =============================
  348. // [MENU HERO WIDGET]
  349. // =============================
  350. Widget _buildHeroSection() {
  351. return Stack(
  352. clipBehavior: Clip.none,
  353. children: [
  354. Container(
  355. width: double.infinity,
  356. height: 290,
  357. decoration: BoxDecoration(
  358. color: Color(0xFFB3E5FC),
  359. borderRadius: BorderRadius.only(
  360. bottomRight: Radius.circular(30),
  361. bottomLeft: Radius.circular(30),
  362. ),
  363. image: DecorationImage(
  364. image: AssetImage(_backgroundImage),
  365. fit: BoxFit.cover,
  366. ),
  367. ),
  368. child: Padding(
  369. padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
  370. child: Column(
  371. crossAxisAlignment: CrossAxisAlignment.start,
  372. mainAxisAlignment: MainAxisAlignment.center,
  373. children: <Widget>[
  374. Text(
  375. 'Assalamu\'alaikum',
  376. style: TextStyle(
  377. fontFamily: 'PoppinsRegular',
  378. color: Colors.white70,
  379. fontSize: 16,
  380. ),
  381. ),
  382. Text(
  383. 'Ngargoyoso',
  384. style: TextStyle(
  385. fontFamily: 'PoppinsSemiBold',
  386. fontSize: 22,
  387. color: Colors.white,
  388. ),
  389. ),
  390. Text(
  391. DateFormat('HH:mm').format(DateTime.now()),
  392. style: TextStyle(
  393. fontFamily: 'PoppinsBold',
  394. fontSize: 50,
  395. height: 1.2,
  396. color: Colors.white,
  397. ),
  398. ),
  399. ],
  400. ),
  401. ),
  402. ),
  403.  
  404. // ========= WAKTU SHOLAT SELANJUTNYA =========
  405. Positioned(
  406. bottom: -55,
  407. left: 20,
  408. right: 20,
  409. child: Container(
  410. decoration: BoxDecoration(
  411. color: Colors.white,
  412. borderRadius: BorderRadius.circular(20),
  413. boxShadow: [
  414. BoxShadow(
  415. blurRadius: 2,
  416. offset: Offset(0, 4),
  417. color: Colors.amber.withOpacity(0.4),
  418. ),
  419. ],
  420. ),
  421. padding: EdgeInsets.symmetric(vertical: 16, horizontal: 14),
  422. child: Column(
  423. children: [
  424. Text(
  425. 'Waktu Sholat Berikutnya..',
  426. style: TextStyle(
  427. fontFamily: 'PoppinsRegular',
  428. fontSize: 14,
  429. color: Colors.grey,
  430. ),
  431. ),
  432. Text(
  433. 'ASHAR',
  434. style: TextStyle(
  435. fontFamily: 'PoppinsBold',
  436. fontSize: 20,
  437. color: Colors.amber,
  438. ),
  439. ),
  440. Text(
  441. '14:22',
  442. style: TextStyle(
  443. fontFamily: 'PoppinsBold',
  444. fontSize: 28,
  445. color: Colors.black38,
  446. ),
  447. ),
  448. Text(
  449. '5 Jam 10 Menit',
  450. style: TextStyle(
  451. fontFamily: 'PoppinsRegular',
  452. fontSize: 13,
  453. color: Colors.grey,
  454. ),
  455. ),
  456. ],
  457. ),
  458. ),
  459. ),
  460. ],
  461. );
  462. }
  463.  
  464. // =============================
  465. // [MENU ITEM WIDGET]
  466. // =============================
  467. Widget _buildMenuItem(String iconPath, String title, String routeName) {
  468. return Material(
  469. color: Colors.transparent,
  470. child: InkWell(
  471. onTap: () {
  472. Navigator.pushNamed(context, routeName);
  473. },
  474. borderRadius: BorderRadius.circular(12),
  475. splashColor: Colors.amber.withOpacity(0.2),
  476. child: Container(
  477. decoration: BoxDecoration(
  478. color: Colors.white,
  479. borderRadius: BorderRadius.circular(12),
  480. boxShadow: [
  481. BoxShadow(
  482. color: Colors.black.withOpacity(0.05),
  483. blurRadius: 4,
  484. offset: const Offset(0, 2),
  485. ),
  486. ],
  487. ),
  488. child: Column(
  489. mainAxisAlignment: MainAxisAlignment.center,
  490. children: [
  491. Image.asset(iconPath, width: 35),
  492. const SizedBox(height: 6),
  493. Text(
  494. title,
  495. style: TextStyle(fontFamily: 'PoppinsRegular', fontSize: 13),
  496. ),
  497. ],
  498. ),
  499. ),
  500. ),
  501. );
  502. }
  503.  
  504. // =============================
  505. // [MENU GRID SECTION WIDGET]
  506. // =============================
  507. Widget _buildMenuGridSection() {
  508. return Padding(
  509. padding: const EdgeInsets.all(8.0),
  510. child: GridView.count(
  511. crossAxisCount: 4, // max 4 baris
  512. shrinkWrap: true,
  513. mainAxisSpacing: 16,
  514. crossAxisSpacing: 16,
  515. physics: const NeverScrollableScrollPhysics(),
  516. children: [
  517. _buildMenuItem(
  518. 'assets/images/ic_menu_doa.png', // iconPath
  519. 'Doa', // title
  520. '/doa',
  521. ), //routeName
  522. _buildMenuItem(
  523. 'assets/images/ic_menu_jadwal_sholat.png',
  524. 'Sholat',
  525. '/doa',
  526. ),
  527. _buildMenuItem(
  528. 'assets/images/ic_menu_video_kajian.png',
  529. 'Kajian',
  530. '/doa',
  531. ),
  532. _buildMenuItem('assets/images/ic_menu_zakat.png', 'Zakat', '/doa'),
  533. _buildMenuItem(
  534. 'assets/images/ic_menu_jadwal_sholat.png', // iconPath
  535. 'Khutbah', // title
  536. '/doa',
  537. ),
  538. ],
  539. ),
  540. );
  541. }
  542.  
  543. // =============================
  544. // [CAROUSEL SECTION WIDGET]
  545. // =============================
  546. Widget _buildCarouselSection() {
  547. return Column(
  548. children: [
  549. const SizedBox(height: 20),
  550. // CAROUSEL CARD
  551. CarouselSlider.builder(
  552. itemCount: posterList.length,
  553. itemBuilder: (context, index, realIndex) {
  554. final poster = posterList[index];
  555. return Container(
  556. margin: EdgeInsets.all(15),
  557. child: ClipRRect(
  558. borderRadius: BorderRadiusGeometry.circular(20),
  559. child: Image.asset(
  560. poster,
  561. width: double.infinity,
  562. fit: BoxFit.cover,
  563. ),
  564. ),
  565. );
  566. },
  567. options: CarouselOptions(
  568. autoPlay: true,
  569. height: 270,
  570. enlargeCenterPage: true,
  571. viewportFraction: 0.7,
  572. onPageChanged: (index, reason) {
  573. setState(() => _currentIndex = index);
  574. },
  575. ),
  576. ),
  577.  
  578. // DOT INDIKATOR CAROUSEL
  579. Row(
  580. mainAxisAlignment: MainAxisAlignment.center,
  581. children: posterList.asMap().entries.map((entry) {
  582. return GestureDetector(
  583. onTap: () => _currentIndex.animateToPage(entry.key),
  584. child: Container(
  585. width: 10,
  586. height: 10,
  587. margin: EdgeInsets.all(2),
  588. decoration: BoxDecoration(
  589. shape: BoxShape.circle,
  590. color: _currentIndex == entry.key
  591. ? Colors.amber
  592. : Colors.grey[400],
  593. ),
  594. ),
  595. );
  596. }).toList(),
  597. ),
  598. ],
  599. );
  600. }
  601. }
  602.  
  603. extension on int {
  604. void animateToPage(int key) {}
  605. }
  606.  
Advertisement
Add Comment
Please, Sign In to add comment