Project
8
.idea/deploymentTargetSelector.xml
generated
@ -4,14 +4,6 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2025-12-18T06:25:18.162928Z">
|
|
||||||
<Target type="DEFAULT_BOOT">
|
|
||||||
<handle>
|
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=325F50919896" />
|
|
||||||
</handle>
|
|
||||||
</Target>
|
|
||||||
</DropdownSelection>
|
|
||||||
<DialogSelection />
|
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
@ -6,11 +6,12 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@drawable/ic_launcher_foreground"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@drawable/ic_launcher_foreground"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.WeatherDemo"
|
android:theme="@style/Theme.WeatherDemo"
|
||||||
@ -33,7 +34,10 @@
|
|||||||
<!-- WAJIB: ChatActivity -->
|
<!-- WAJIB: ChatActivity -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.ChatActivity"
|
android:name=".ui.ChatActivity"
|
||||||
android:exported="true" />
|
android:theme="@style/Theme.WeatherDemo.NoActionBar"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
@ -1,22 +1,19 @@
|
|||||||
package com.example.weatherdemo.data.api
|
package com.example.weatherdemo.data.api
|
||||||
|
|
||||||
|
import com.example.weatherdemo.data.models.WeatherForecastResponse
|
||||||
import com.example.weatherdemo.data.models.WeatherResponse
|
import com.example.weatherdemo.data.models.WeatherResponse
|
||||||
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrofit service interface for Weather API
|
* Retrofit service interface for Weather API (WeatherAPI.com)
|
||||||
* Defines the API endpoints and request methods
|
* Defines the API endpoints and request methods
|
||||||
*/
|
*/
|
||||||
interface WeatherApiService {
|
interface WeatherApiService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches current weather data for a specific location
|
* Fetches current weather data for a specific location
|
||||||
* @param key API key for authentication
|
|
||||||
* @param query Location query (city name, zip code, etc.)
|
|
||||||
* @param aqi Whether to include air quality index (yes/no)
|
|
||||||
*/
|
*/
|
||||||
@GET("current.json")
|
@GET("current.json")
|
||||||
suspend fun getCurrentWeather(
|
suspend fun getCurrentWeather(
|
||||||
@ -24,4 +21,17 @@ interface WeatherApiService {
|
|||||||
@Query("q") query: String,
|
@Query("q") query: String,
|
||||||
@Query("aqi") aqi: String = "yes"
|
@Query("aqi") aqi: String = "yes"
|
||||||
): Response<WeatherResponse>
|
): Response<WeatherResponse>
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Fetches forecast (current + hourly + daily) for a specific location
|
||||||
|
* @param days number of days forecast
|
||||||
|
*/
|
||||||
|
@GET("forecast.json")
|
||||||
|
suspend fun getForecastWeather(
|
||||||
|
@Query("key") key: String,
|
||||||
|
@Query("q") query: String,
|
||||||
|
@Query("days") days: Int = 3,
|
||||||
|
@Query("aqi") aqi: String = "no",
|
||||||
|
@Query("alerts") alerts: String = "no"
|
||||||
|
): Response<WeatherForecastResponse>
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.example.weatherdemo.data.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response model for WeatherAPI "forecast.json".
|
||||||
|
* This includes current weather + hourly & daily forecast.
|
||||||
|
*/
|
||||||
|
data class WeatherForecastResponse(
|
||||||
|
val location: Location,
|
||||||
|
val current: Current,
|
||||||
|
val forecast: Forecast
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Forecast(
|
||||||
|
val forecastday: List<ForecastDay>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ForecastDay(
|
||||||
|
val date: String,
|
||||||
|
val day: Day,
|
||||||
|
val hour: List<Hour>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Day(
|
||||||
|
val maxtemp_c: Double,
|
||||||
|
val mintemp_c: Double,
|
||||||
|
val avgtemp_c: Double,
|
||||||
|
val condition: Condition,
|
||||||
|
val daily_chance_of_rain: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Hour(
|
||||||
|
val time: String,
|
||||||
|
val temp_c: Double,
|
||||||
|
val condition: Condition,
|
||||||
|
val chance_of_rain: Int? = null
|
||||||
|
)
|
||||||
@ -1,7 +1,7 @@
|
|||||||
package com.example.weatherdemo.data.repository
|
package com.example.weatherdemo.data.repository
|
||||||
|
|
||||||
import com.example.weatherdemo.data.api.WeatherApiService
|
import com.example.weatherdemo.data.api.WeatherApiService
|
||||||
import com.example.weatherdemo.data.models.WeatherResponse
|
import com.example.weatherdemo.data.models.WeatherForecastResponse
|
||||||
import com.example.weatherdemo.utils.Result
|
import com.example.weatherdemo.utils.Result
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
|
||||||
@ -12,23 +12,24 @@ import retrofit2.Response
|
|||||||
class WeatherRepository(private val apiService: WeatherApiService) {
|
class WeatherRepository(private val apiService: WeatherApiService) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches weather data from API and returns a Result wrapper
|
* Fetches forecast weather data from API and returns a Result wrapper.
|
||||||
* @param location The location to get weather for
|
* This uses WeatherAPI "forecast.json" so we can show hourly/daily prediction on main page.
|
||||||
* @return Result object containing either success or error
|
*
|
||||||
|
* @param location The location to get weather for (city name, zip, lat/long)
|
||||||
|
* @param days number of forecast days (default 3)
|
||||||
*/
|
*/
|
||||||
suspend fun getWeatherData(location: String): Result<WeatherResponse> {
|
suspend fun getWeatherForecast(location: String, days: Int = 3): Result<WeatherForecastResponse> {
|
||||||
return try {
|
return try {
|
||||||
// Make API call
|
val response: Response<WeatherForecastResponse> = apiService.getForecastWeather(
|
||||||
val response: Response<WeatherResponse> = apiService.getCurrentWeather(
|
|
||||||
key = "822615b3cef1437bb0202739251712", // API key
|
key = "822615b3cef1437bb0202739251712", // API key
|
||||||
query = location
|
query = location,
|
||||||
|
days = days
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if response is successful
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val weatherResponse = response.body()
|
val body = response.body()
|
||||||
if (weatherResponse != null) {
|
if (body != null) {
|
||||||
Result.Success(weatherResponse)
|
Result.Success(body)
|
||||||
} else {
|
} else {
|
||||||
Result.Error(Exception("Empty response body"))
|
Result.Error(Exception("Empty response body"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,12 @@ package com.example.weatherdemo.ui
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.weatherdemo.R
|
import com.example.weatherdemo.R
|
||||||
@ -20,64 +24,67 @@ class ChatActivity : AppCompatActivity() {
|
|||||||
private lateinit var btnSend: Button
|
private lateinit var btnSend: Button
|
||||||
private lateinit var recyclerView: RecyclerView
|
private lateinit var recyclerView: RecyclerView
|
||||||
private lateinit var chatAdapter: ChatAdapter
|
private lateinit var chatAdapter: ChatAdapter
|
||||||
private lateinit var username: String // username user aktif
|
private lateinit var username: String
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// ✅ penting: kita handle insets sendiri biar semua HP aman
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContentView(R.layout.activity_chat)
|
setContentView(R.layout.activity_chat)
|
||||||
|
|
||||||
// Toolbar + tombol back
|
|
||||||
val toolbar = findViewById<Toolbar>(R.id.toolbarChat)
|
|
||||||
setSupportActionBar(toolbar)
|
|
||||||
toolbar.setNavigationOnClickListener { finish() }
|
|
||||||
|
|
||||||
// ✅ Ambil username dari Intent
|
|
||||||
username = intent.getStringExtra("username") ?: "Guest"
|
username = intent.getStringExtra("username") ?: "Guest"
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.btnBack).setOnClickListener { finish() }
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() { finish() }
|
||||||
|
})
|
||||||
|
|
||||||
|
val root = findViewById<androidx.constraintlayout.widget.ConstraintLayout>(R.id.chatRoot)
|
||||||
|
|
||||||
|
// ✅ INI INTI FIX: kalau keyboard muncul, kasih padding bawah sebesar tinggi keyboard
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
|
||||||
|
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
|
||||||
|
val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
|
||||||
|
// padding atas biar tidak nabrak status bar, padding bawah ikut keyboard
|
||||||
|
v.setPadding(
|
||||||
|
v.paddingLeft,
|
||||||
|
sysBars.top,
|
||||||
|
v.paddingRight,
|
||||||
|
ime.bottom
|
||||||
|
)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
etMessage = findViewById(R.id.etMessage)
|
etMessage = findViewById(R.id.etMessage)
|
||||||
btnSend = findViewById(R.id.btnSend)
|
btnSend = findViewById(R.id.btnSend)
|
||||||
recyclerView = findViewById(R.id.recyclerView)
|
recyclerView = findViewById(R.id.recyclerView)
|
||||||
|
|
||||||
val layoutManager = LinearLayoutManager(this).apply {
|
recyclerView.layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
|
||||||
stackFromEnd = true
|
|
||||||
}
|
|
||||||
recyclerView.layoutManager = layoutManager
|
|
||||||
|
|
||||||
// 🔥 WAJIB kirim username ke ChatAdapter
|
|
||||||
chatAdapter = ChatAdapter(username)
|
chatAdapter = ChatAdapter(username)
|
||||||
recyclerView.adapter = chatAdapter
|
recyclerView.adapter = chatAdapter
|
||||||
|
|
||||||
btnSend.setOnClickListener {
|
btnSend.setOnClickListener {
|
||||||
val messageText = etMessage.text.toString().trim()
|
val messageText = etMessage.text.toString().trim()
|
||||||
|
|
||||||
if (messageText.isNotEmpty()) {
|
if (messageText.isNotEmpty()) {
|
||||||
|
val time = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
|
||||||
val time = SimpleDateFormat(
|
FirebaseUtils.sendMessage(username = username, message = messageText, time = time)
|
||||||
"HH:mm",
|
|
||||||
Locale.getDefault()
|
|
||||||
).format(Date())
|
|
||||||
|
|
||||||
FirebaseUtils.sendMessage(
|
|
||||||
username = username,
|
|
||||||
message = messageText,
|
|
||||||
time = time
|
|
||||||
)
|
|
||||||
|
|
||||||
etMessage.setText("")
|
etMessage.setText("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biar insets langsung diterapkan
|
||||||
|
ViewCompat.requestApplyInsets(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
|
||||||
FirebaseUtils.getMessages { messages ->
|
FirebaseUtils.getMessages { messages ->
|
||||||
chatAdapter.submitList(messages)
|
chatAdapter.submitList(messages)
|
||||||
|
|
||||||
if (messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
recyclerView.post {
|
recyclerView.post { recyclerView.scrollToPosition(messages.size - 1) }
|
||||||
recyclerView.scrollToPosition(messages.size - 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,38 +2,33 @@ package com.example.weatherdemo.ui
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.example.weatherdemo.R
|
import com.example.weatherdemo.R
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var etUsername: EditText
|
|
||||||
private lateinit var btnLogin: Button
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_login)
|
setContentView(R.layout.activity_login)
|
||||||
|
|
||||||
etUsername = findViewById(R.id.etUsername)
|
val etUsername = findViewById<EditText>(R.id.etUsername)
|
||||||
btnLogin = findViewById(R.id.btnLogin)
|
val btnLogin = findViewById<MaterialButton>(R.id.btnLogin)
|
||||||
|
|
||||||
btnLogin.setOnClickListener {
|
btnLogin.setOnClickListener {
|
||||||
val username = etUsername.text.toString().trim()
|
val username = etUsername.text.toString().trim()
|
||||||
|
|
||||||
if (username.isNotEmpty()) {
|
if (username.isEmpty()) {
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
Toast.makeText(this, "Masukkan username dulu", Toast.LENGTH_SHORT).show()
|
||||||
|
return@setOnClickListener
|
||||||
// ✅ FIX: key KONSISTEN
|
|
||||||
intent.putExtra("username", username)
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
Toast.makeText(this, "Please enter a username", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.putExtra("username", username)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,23 +6,25 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.ImageButton
|
||||||
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.lifecycle.Observer
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.example.weatherdemo.R
|
import com.example.weatherdemo.R
|
||||||
import com.example.weatherdemo.data.api.RetrofitInstance
|
import com.example.weatherdemo.data.api.RetrofitInstance
|
||||||
import com.example.weatherdemo.data.models.WeatherResponse
|
import com.example.weatherdemo.data.models.WeatherForecastResponse
|
||||||
import com.example.weatherdemo.data.repository.WeatherRepository
|
import com.example.weatherdemo.data.repository.WeatherRepository
|
||||||
|
import com.example.weatherdemo.ui.adapter.DailyForecastAdapter
|
||||||
|
import com.example.weatherdemo.ui.adapter.HourlyForecastAdapter
|
||||||
import com.example.weatherdemo.viewmodel.WeatherViewModel
|
import com.example.weatherdemo.viewmodel.WeatherViewModel
|
||||||
import com.example.weatherdemo.viewmodel.WeatherViewModelFactory
|
import com.example.weatherdemo.viewmodel.WeatherViewModelFactory
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@ -30,26 +32,43 @@ class MainActivity : AppCompatActivity() {
|
|||||||
WeatherViewModelFactory(WeatherRepository(RetrofitInstance.apiService))
|
WeatherViewModelFactory(WeatherRepository(RetrofitInstance.apiService))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lateinit var hourlyAdapter: HourlyForecastAdapter
|
||||||
|
private lateinit var dailyAdapter: DailyForecastAdapter
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
// ✅ FIX DI SINI SAJA
|
|
||||||
val username = intent.getStringExtra("username") ?: "Guest"
|
val username = intent.getStringExtra("username") ?: "Guest"
|
||||||
|
findViewById<TextView>(R.id.tvWelcomeMessage).text = "Welcome, $username!"
|
||||||
|
|
||||||
val tvWelcomeMessage = findViewById<TextView>(R.id.tvWelcomeMessage)
|
// ✅ CHAT GLOBAL → ImageButton (BUKAN FloatingActionButton)
|
||||||
tvWelcomeMessage.text = "Welcome, $username!"
|
findViewById<ImageButton>(R.id.fabChat).setOnClickListener {
|
||||||
|
|
||||||
// FAB chat → kirim username ke ChatActivity
|
|
||||||
findViewById<FloatingActionButton>(R.id.fabChat).setOnClickListener {
|
|
||||||
val i = Intent(this, ChatActivity::class.java)
|
val i = Intent(this, ChatActivity::class.java)
|
||||||
i.putExtra("username", username)
|
i.putExtra("username", username)
|
||||||
startActivity(i)
|
startActivity(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initForecastLists()
|
||||||
initViews()
|
initViews()
|
||||||
setupObservers()
|
setupObservers()
|
||||||
viewModel.fetchWeatherData("London")
|
|
||||||
|
// default city (AMAN)
|
||||||
|
viewModel.fetchForecast("Bekasi", days = 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initForecastLists() {
|
||||||
|
hourlyAdapter = HourlyForecastAdapter()
|
||||||
|
findViewById<RecyclerView>(R.id.rvHourlyForecast).apply {
|
||||||
|
layoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.HORIZONTAL, false)
|
||||||
|
adapter = hourlyAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
dailyAdapter = DailyForecastAdapter()
|
||||||
|
findViewById<RecyclerView>(R.id.rvDailyForecast).apply {
|
||||||
|
layoutManager = LinearLayoutManager(this@MainActivity)
|
||||||
|
adapter = dailyAdapter
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
@ -60,7 +79,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val location = etSearch.text.toString().trim()
|
val location = etSearch.text.toString().trim()
|
||||||
if (location.isNotEmpty()) {
|
if (location.isNotEmpty()) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
viewModel.fetchWeatherData(location)
|
viewModel.fetchForecast(location, days = 3)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, "Please enter a location", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "Please enter a location", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@ -68,78 +87,73 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupObservers() {
|
private fun setupObservers() {
|
||||||
viewModel.weatherData.observe(this, Observer { weatherData ->
|
viewModel.forecastData.observe(this) { forecast ->
|
||||||
weatherData?.let { updateWeatherUI(it) }
|
forecast?.let { updateWeatherUI(it) }
|
||||||
})
|
}
|
||||||
|
|
||||||
viewModel.isLoading.observe(this, Observer { isLoading ->
|
viewModel.isLoading.observe(this) { isLoading ->
|
||||||
findViewById<ProgressBar>(R.id.progressBar).visibility =
|
findViewById<ProgressBar>(R.id.progressBar).visibility =
|
||||||
if (isLoading) View.VISIBLE else View.GONE
|
if (isLoading) View.VISIBLE else View.GONE
|
||||||
})
|
}
|
||||||
|
|
||||||
viewModel.errorMessage.observe(this, Observer { errorMessage ->
|
viewModel.errorMessage.observe(this) { errorMessage ->
|
||||||
val tvError = findViewById<TextView>(R.id.tvError)
|
findViewById<TextView>(R.id.tvError).apply {
|
||||||
if (errorMessage.isNotEmpty()) {
|
text = errorMessage
|
||||||
tvError.text = errorMessage
|
visibility = if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE
|
||||||
tvError.visibility = View.VISIBLE
|
|
||||||
findViewById<MaterialCardView>(R.id.weatherCard).visibility = View.GONE
|
|
||||||
Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
tvError.visibility = View.GONE
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateWeatherUI(weatherResponse: WeatherResponse) {
|
|
||||||
val weatherCard = findViewById<MaterialCardView>(R.id.weatherCard)
|
|
||||||
val tvLocation = findViewById<TextView>(R.id.tvLocation)
|
|
||||||
val tvTemperature = findViewById<TextView>(R.id.tvTemperature)
|
|
||||||
val tvCondition = findViewById<TextView>(R.id.tvCondition)
|
|
||||||
val tvFeelsLike = findViewById<TextView>(R.id.tvFeelsLike)
|
|
||||||
val tvHumidity = findViewById<TextView>(R.id.tvHumidity)
|
|
||||||
val tvWind = findViewById<TextView>(R.id.tvWind)
|
|
||||||
|
|
||||||
tvLocation.text = "${weatherResponse.location.name}, ${weatherResponse.location.country}"
|
|
||||||
tvTemperature.text = "${weatherResponse.current.temp_c}°C"
|
|
||||||
tvCondition.text = weatherResponse.current.condition.text
|
|
||||||
tvFeelsLike.text = "${weatherResponse.current.feelslike_c}°C"
|
|
||||||
tvHumidity.text = "${weatherResponse.current.humidity}%"
|
|
||||||
tvWind.text = "${weatherResponse.current.wind_kph} km/h"
|
|
||||||
|
|
||||||
val condition = weatherResponse.current.condition.text.lowercase()
|
|
||||||
val mainLayout = findViewById<LinearLayout>(R.id.mainLayout)
|
|
||||||
|
|
||||||
when {
|
|
||||||
condition.contains("clear") || condition.contains("sunny") -> {
|
|
||||||
weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.sunny))
|
|
||||||
mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.sunny_background))
|
|
||||||
}
|
|
||||||
condition.contains("rain") -> {
|
|
||||||
weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.rainy))
|
|
||||||
mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.rainy_background))
|
|
||||||
}
|
|
||||||
condition.contains("cloudy") -> {
|
|
||||||
weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.cloudy))
|
|
||||||
mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.cloudy_background))
|
|
||||||
}
|
|
||||||
condition.contains("snow") -> {
|
|
||||||
weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.snowy))
|
|
||||||
mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.snowy_background))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
weatherCard.setCardBackgroundColor(ContextCompat.getColor(this, R.color.default_weather))
|
|
||||||
mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.default_weather_background))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateWeatherUI(weather: WeatherForecastResponse) {
|
||||||
|
val weatherCard = findViewById<MaterialCardView>(R.id.weatherCard)
|
||||||
|
val ivBg = findViewById<ImageView>(R.id.ivBg)
|
||||||
|
|
||||||
|
findViewById<TextView>(R.id.tvLocation).text =
|
||||||
|
"${weather.location.name}, ${weather.location.country}"
|
||||||
|
findViewById<TextView>(R.id.tvTemperature).text =
|
||||||
|
"${weather.current.temp_c}°C"
|
||||||
|
findViewById<TextView>(R.id.tvCondition).text =
|
||||||
|
weather.current.condition.text
|
||||||
|
findViewById<TextView>(R.id.tvFeelsLike).text =
|
||||||
|
"${weather.current.feelslike_c}°C"
|
||||||
|
findViewById<TextView>(R.id.tvHumidity).text =
|
||||||
|
"${weather.current.humidity}%"
|
||||||
|
findViewById<TextView>(R.id.tvWind).text =
|
||||||
|
"${weather.current.wind_kph} km/h"
|
||||||
|
|
||||||
|
// ✅ BACKGROUND FIX (no typo)
|
||||||
|
val condition = weather.current.condition.text.lowercase()
|
||||||
|
when {
|
||||||
|
condition.contains("sunny") || condition.contains("clear") ->
|
||||||
|
ivBg.setImageResource(R.drawable.sunny)
|
||||||
|
|
||||||
|
condition.contains("rain") || condition.contains("drizzle") || condition.contains("thunder") ->
|
||||||
|
ivBg.setImageResource(R.drawable.rainy)
|
||||||
|
|
||||||
|
condition.contains("cloud") || condition.contains("overcast") || condition.contains("mist") || condition.contains("fog") ->
|
||||||
|
ivBg.setImageResource(R.drawable.cloudy)
|
||||||
|
|
||||||
|
condition.contains("snow") || condition.contains("sleet") || condition.contains("blizzard") ->
|
||||||
|
ivBg.setImageResource(R.drawable.snowy)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
ivBg.setImageResource(R.drawable.defaultt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// forecast
|
||||||
|
val hourly = weather.forecast.forecastday.firstOrNull()?.hour.orEmpty().take(12)
|
||||||
|
hourlyAdapter.submitList(hourly)
|
||||||
|
|
||||||
|
dailyAdapter.submitList(weather.forecast.forecastday)
|
||||||
|
|
||||||
weatherCard.visibility = View.VISIBLE
|
weatherCard.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideKeyboard() {
|
private fun hideKeyboard() {
|
||||||
val view = this.currentFocus
|
currentFocus?.let {
|
||||||
view?.let {
|
|
||||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.hideSoftInputFromWindow(it.windowToken, 0)
|
imm.hideSoftInputFromWindow(it.windowToken, 0)
|
||||||
|
it.clearFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
package com.example.weatherdemo.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.weatherdemo.R
|
||||||
|
import com.example.weatherdemo.data.models.ForecastDay
|
||||||
|
|
||||||
|
class DailyForecastAdapter(
|
||||||
|
private var items: List<ForecastDay> = emptyList()
|
||||||
|
) : RecyclerView.Adapter<DailyForecastAdapter.DayVH>() {
|
||||||
|
|
||||||
|
fun submitList(newItems: List<ForecastDay>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DayVH {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_day_forecast, parent, false)
|
||||||
|
return DayVH(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DayVH, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class DayVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tvDate: TextView = itemView.findViewById(R.id.tvDate)
|
||||||
|
private val tvRange: TextView = itemView.findViewById(R.id.tvTempRange)
|
||||||
|
private val tvCondition: TextView = itemView.findViewById(R.id.tvDayCondition)
|
||||||
|
|
||||||
|
fun bind(item: ForecastDay) {
|
||||||
|
tvDate.text = item.date
|
||||||
|
tvRange.text = "${item.day.mintemp_c}°C - ${item.day.maxtemp_c}°C"
|
||||||
|
tvCondition.text = item.day.condition.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.example.weatherdemo.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.example.weatherdemo.R
|
||||||
|
import com.example.weatherdemo.data.models.Hour
|
||||||
|
|
||||||
|
class HourlyForecastAdapter(
|
||||||
|
private var items: List<Hour> = emptyList()
|
||||||
|
) : RecyclerView.Adapter<HourlyForecastAdapter.HourVH>() {
|
||||||
|
|
||||||
|
fun submitList(newItems: List<Hour>) {
|
||||||
|
items = newItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HourVH {
|
||||||
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.item_hour_forecast, parent, false)
|
||||||
|
return HourVH(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: HourVH, position: Int) {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = items.size
|
||||||
|
|
||||||
|
class HourVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val tvHour: TextView = itemView.findViewById(R.id.tvHour)
|
||||||
|
private val tvTemp: TextView = itemView.findViewById(R.id.tvHourTemp)
|
||||||
|
private val tvCondition: TextView = itemView.findViewById(R.id.tvHourCondition)
|
||||||
|
|
||||||
|
fun bind(item: Hour) {
|
||||||
|
// item.time example: "2025-12-29 13:00"
|
||||||
|
val hourText = item.time.substringAfter(" ").ifBlank { item.time }
|
||||||
|
tvHour.text = hourText
|
||||||
|
tvTemp.text = "${item.temp_c}°C"
|
||||||
|
tvCondition.text = item.condition.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.weatherdemo.data.models.WeatherResponse
|
import com.example.weatherdemo.data.models.WeatherForecastResponse
|
||||||
import com.example.weatherdemo.data.repository.WeatherRepository
|
import com.example.weatherdemo.data.repository.WeatherRepository
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import com.example.weatherdemo.utils.Result
|
import com.example.weatherdemo.utils.Result
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel class that prepares and manages UI-related data
|
* ViewModel class that prepares and manages UI-related data
|
||||||
@ -16,9 +16,9 @@ import com.example.weatherdemo.utils.Result
|
|||||||
*/
|
*/
|
||||||
class WeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
|
class WeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
|
||||||
|
|
||||||
// LiveData for weather information - observed by UI
|
// LiveData for forecast response (current + hourly + daily)
|
||||||
private val _weatherData = MutableLiveData<WeatherResponse>()
|
private val _forecastData = MutableLiveData<WeatherForecastResponse>()
|
||||||
val weatherData: LiveData<WeatherResponse> = _weatherData
|
val forecastData: LiveData<WeatherForecastResponse> = _forecastData
|
||||||
|
|
||||||
// LiveData for loading state - to show/hide progress bar
|
// LiveData for loading state - to show/hide progress bar
|
||||||
private val _isLoading = MutableLiveData<Boolean>()
|
private val _isLoading = MutableLiveData<Boolean>()
|
||||||
@ -29,17 +29,15 @@ class WeatherViewModel(private val repository: WeatherRepository) : ViewModel()
|
|||||||
val errorMessage: LiveData<String> = _errorMessage
|
val errorMessage: LiveData<String> = _errorMessage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches weather data for a given location
|
* Fetches forecast data for a given location
|
||||||
* Uses coroutines for background operations
|
|
||||||
* @param location The city name to search for
|
|
||||||
*/
|
*/
|
||||||
fun fetchWeatherData(location: String) {
|
fun fetchForecast(location: String, days: Int = 3) {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = repository.getWeatherData(location)) {
|
when (val result = repository.getWeatherForecast(location, days)) {
|
||||||
is Result.Success -> {
|
is Result.Success -> {
|
||||||
_weatherData.value = result.data
|
_forecastData.value = result.data
|
||||||
_errorMessage.value = ""
|
_errorMessage.value = ""
|
||||||
}
|
}
|
||||||
is Result.Error -> {
|
is Result.Error -> {
|
||||||
@ -49,4 +47,4 @@ class WeatherViewModel(private val repository: WeatherRepository) : ViewModel()
|
|||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
app/src/main/res/drawable/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
@ -1,53 +1,90 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/chatRoot"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:orientation="vertical"
|
|
||||||
android:background="@drawable/background_gradient">
|
|
||||||
|
|
||||||
<!-- TOOLBAR + TOMBOL BACK -->
|
<!-- TOP BAR -->
|
||||||
<androidx.appcompat.widget.Toolbar
|
<RelativeLayout
|
||||||
android:id="@+id/toolbarChat"
|
android:id="@+id/topBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="56dp"
|
android:layout_height="56dp"
|
||||||
android:background="@color/button_color"
|
android:background="#2F55F4"
|
||||||
app:title="Global Chat"
|
android:paddingStart="12dp"
|
||||||
app:titleTextColor="@android:color/white"
|
android:paddingEnd="12dp"
|
||||||
app:navigationIcon="@android:drawable/ic_media_previous" />
|
android:gravity="center_vertical"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<!-- RecyclerView -->
|
<TextView
|
||||||
|
android:id="@+id/btnBack"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="←"
|
||||||
|
android:textSize="26sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_toEndOf="@id/btnBack"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="Chat Global"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<!-- CHAT LIST -->
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:scrollbars="vertical"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
/>
|
app:layout_constraintTop_toBottomOf="@id/topBar"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/inputBar"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<!-- Input -->
|
<!-- INPUT BAR -->
|
||||||
<EditText
|
<LinearLayout
|
||||||
android:id="@+id/etMessage"
|
android:id="@+id/inputBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="Type a message"
|
android:orientation="horizontal"
|
||||||
android:inputType="text"
|
android:padding="8dp"
|
||||||
android:padding="10dp"
|
android:background="#F2F2F2"
|
||||||
android:layout_marginTop="8dp"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:background="@android:color/white" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<!-- Send -->
|
<EditText
|
||||||
<Button
|
android:id="@+id/etMessage"
|
||||||
android:id="@+id/btnSend"
|
android:layout_width="0dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="44dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_weight="1"
|
||||||
android:text="Send"
|
android:textColor="@android:color/black"
|
||||||
android:layout_marginTop="12dp"
|
android:textColorHint="#88000000"
|
||||||
android:layout_marginBottom="16dp"
|
android:hint="Type a message"
|
||||||
android:textColor="@android:color/white"
|
android:background="@android:color/white"
|
||||||
android:background="@color/button_color" />
|
android:padding="12dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
<Button
|
||||||
|
android:id="@+id/btnSend"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:text="Send"
|
||||||
|
android:textAllCaps="false"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:backgroundTint="#2F55F4"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -1,28 +1,95 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:background="@android:color/white">
|
||||||
android:padding="16dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="@drawable/background_gradient">
|
|
||||||
|
|
||||||
<!-- Username Input Field -->
|
<ImageView
|
||||||
<EditText
|
android:id="@+id/ivLogo"
|
||||||
android:id="@+id/etUsername"
|
android:layout_width="320dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="320dp"
|
||||||
|
android:src="@drawable/ic_weatherhub"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:contentDescription="WeatherHub Logo"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/tvAppName"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:layout_marginBottom="6dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvAppName"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="Enter username"
|
android:text="WeatherHub"
|
||||||
android:textColor="@color/black"
|
android:textStyle="bold"
|
||||||
android:textColorHint="@color/gray"
|
android:textSize="22sp"
|
||||||
android:background="@android:color/white"
|
android:textColor="@android:color/black"
|
||||||
android:padding="12dp" />
|
app:layout_constraintTop_toBottomOf="@id/ivLogo"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/cardUsername"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:layout_marginBottom="44dp"/>
|
||||||
|
|
||||||
<!-- Login Button -->
|
<!-- INPUT = ukuran sama dengan button -->
|
||||||
<Button
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/cardUsername"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginStart="44dp"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
app:cardCornerRadius="28dp"
|
||||||
|
app:cardElevation="10dp"
|
||||||
|
app:cardUseCompatPadding="false"
|
||||||
|
app:cardBackgroundColor="#EEF0FF"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/tvAppName"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/btnLogin"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etUsername"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minHeight="56dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="22dp"
|
||||||
|
android:paddingEnd="22dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:hint=" "
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:textColorHint="#8A8A8A"
|
||||||
|
android:includeFontPadding="false"/>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- BUTTON = sama height & radius -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnLogin"
|
android:id="@+id/btnLogin"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="56dp"
|
||||||
|
android:layout_marginStart="44dp"
|
||||||
|
android:layout_marginEnd="44dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
android:text="Login"
|
android:text="Login"
|
||||||
android:layout_marginTop="20dp" />
|
android:textAllCaps="false"
|
||||||
</LinearLayout>
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
app:cornerRadius="28dp"
|
||||||
|
app:elevation="10dp"
|
||||||
|
app:backgroundTint="#2F55F4"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/cardUsername"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:layout_marginBottom="90dp"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|||||||
@ -2,196 +2,325 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:background="@drawable/background_gradient">
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivBg"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/defaultt"
|
||||||
|
android:contentDescription="Background" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#14000000" />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:padding="16dp">
|
android:fillViewport="true"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/mainLayout"
|
android:id="@+id/mainLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:background="@color/solid_background_color">
|
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Welcome -->
|
||||||
<androidx.cardview.widget.CardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="32dp"
|
android:layout_marginBottom="14dp"
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="18dp"
|
||||||
app:cardElevation="8dp"
|
app:cardElevation="8dp"
|
||||||
app:cardBackgroundColor="@color/welcome_card_background">
|
app:cardUseCompatPadding="false"
|
||||||
|
app:strokeWidth="0dp"
|
||||||
|
app:cardBackgroundColor="#2F55F4">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvWelcomeMessage"
|
android:id="@+id/tvWelcomeMessage"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:text="Welcome!"
|
android:text="Welcome!"
|
||||||
android:textColor="@color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="22sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
</androidx.cardview.widget.CardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="24dp"
|
android:layout_marginBottom="14dp"
|
||||||
app:cardCornerRadius="16dp"
|
app:cardCornerRadius="18dp"
|
||||||
app:cardElevation="8dp"
|
app:cardElevation="10dp"
|
||||||
app:cardBackgroundColor="@color/card_background">
|
app:cardUseCompatPadding="false"
|
||||||
|
app:strokeWidth="1dp"
|
||||||
|
app:strokeColor="#22FFFFFF"
|
||||||
|
app:cardBackgroundColor="#55FFFFFF">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="16dp">
|
android:padding="14dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/etSearch"
|
android:id="@+id/etSearch"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:hint="Enter city name"
|
android:hint="Enter city name"
|
||||||
android:textColor="@color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textColorHint="@color/hint_color"
|
android:textColorHint="#CCFFFFFF"
|
||||||
android:background="@android:color/transparent" />
|
android:background="@android:color/transparent"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/btnSearch"
|
android:id="@+id/btnSearch"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="44dp"
|
||||||
android:text="Search"
|
android:text="Search"
|
||||||
android:textColor="@color/white"
|
android:textAllCaps="false"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
app:cornerRadius="0dp"
|
||||||
app:icon="@drawable/ic_search"
|
app:icon="@drawable/ic_search"
|
||||||
android:backgroundTint="@color/button_color" />
|
app:iconPadding="8dp"
|
||||||
|
app:backgroundTint="#2F55F4" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Progress -->
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progressBar"
|
android:id="@+id/progressBar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
|
||||||
<!-- Error -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvError"
|
android:id="@+id/tvError"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/error_color"
|
android:textColor="#FFFF4444"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="10dp" />
|
||||||
|
|
||||||
<!-- Weather Data Card (ID WAJIB sama seperti di MainActivity.kt) -->
|
<!-- WEATHER OUTER -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/weatherCard"
|
android:id="@+id/weatherCard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:cardCornerRadius="24dp"
|
app:cardCornerRadius="22dp"
|
||||||
app:cardElevation="12dp"
|
app:cardElevation="0dp"
|
||||||
app:cardBackgroundColor="@color/weather_card_background">
|
app:cardUseCompatPadding="false"
|
||||||
|
app:strokeWidth="0dp"
|
||||||
|
app:cardBackgroundColor="@android:color/transparent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:padding="24dp">
|
|
||||||
|
|
||||||
<TextView
|
<!-- BOX 1 -->
|
||||||
android:id="@+id/tvLocation"
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Location"
|
app:cardCornerRadius="18dp"
|
||||||
android:textSize="24sp"
|
app:cardElevation="10dp"
|
||||||
android:textStyle="bold"
|
app:cardUseCompatPadding="false"
|
||||||
android:textColor="@color/white"
|
app:strokeWidth="1dp"
|
||||||
android:gravity="center"
|
app:strokeColor="#22FFFFFF"
|
||||||
android:layout_marginBottom="8dp" />
|
app:cardBackgroundColor="#26FFFFFF">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tvTemperature"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLocation"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Location"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="6dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTemperature"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="--°C"
|
||||||
|
android:textSize="44sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvCondition"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Condition"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="14dp" />
|
||||||
|
|
||||||
|
<GridLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:columnCount="2">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Feels Like:"
|
||||||
|
android:textColor="#E6FFFFFF"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvFeelsLike"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="--°C"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:layout_gravity="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Humidity:"
|
||||||
|
android:textColor="#E6FFFFFF"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHumidity"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="--%"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:layout_gravity="end" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Wind:"
|
||||||
|
android:textColor="#E6FFFFFF"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvWind"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="-- km/h"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:layout_gravity="end" />
|
||||||
|
</GridLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp" />
|
||||||
|
|
||||||
|
<!-- BOX 2 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="--°C"
|
app:cardCornerRadius="18dp"
|
||||||
android:textSize="48sp"
|
app:cardElevation="2dp"
|
||||||
android:textStyle="bold"
|
app:cardUseCompatPadding="false"
|
||||||
android:textColor="@color/white"
|
app:strokeWidth="1dp"
|
||||||
android:gravity="center"
|
app:strokeColor="#22FFFFFF"
|
||||||
android:layout_marginBottom="16dp" />
|
app:cardBackgroundColor="#26FFFFFF">
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/tvCondition"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Prediksi per Jam (12 Jam ke Depan)"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvHourlyForecast"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp" />
|
||||||
|
|
||||||
|
<!-- BOX 3 -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Condition"
|
app:cardCornerRadius="18dp"
|
||||||
android:textSize="18sp"
|
app:cardElevation="2dp"
|
||||||
android:textColor="@color/white"
|
app:cardUseCompatPadding="false"
|
||||||
android:gravity="center"
|
app:strokeWidth="1dp"
|
||||||
android:layout_marginBottom="24dp" />
|
app:strokeColor="#22FFFFFF"
|
||||||
|
app:cardBackgroundColor="#26FFFFFF">
|
||||||
|
|
||||||
<GridLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:columnCount="2">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Feels Like:"
|
android:orientation="vertical"
|
||||||
android:textColor="@color/label_color"
|
android:padding="16dp">
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvFeelsLike"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:text="Prediksi per Hari (3 Hari)"
|
||||||
android:text="--°C"
|
android:textColor="@android:color/white"
|
||||||
android:textColor="@color/white"
|
android:textStyle="bold"
|
||||||
android:textSize="14sp"
|
android:textSize="15sp"
|
||||||
android:layout_gravity="end" />
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
<TextView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/rvDailyForecast"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:text="Humidity:"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/label_color"
|
android:overScrollMode="never" />
|
||||||
android:textSize="14sp" />
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvHumidity"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="--%"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:layout_gravity="end" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Wind:"
|
|
||||||
android:textColor="@color/label_color"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvWind"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="-- km/h"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:layout_gravity="end" />
|
|
||||||
</GridLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@ -199,15 +328,16 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<!-- FAB CHAT GLOBAL -->
|
<!-- Chat Global: pakai gambar dari drawable -->
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<ImageButton
|
||||||
android:id="@+id/fabChat"
|
android:id="@+id/fabChat"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="66dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="66dp"
|
||||||
android:layout_gravity="bottom|end"
|
android:layout_gravity="bottom|end"
|
||||||
android:layout_margin="20dp"
|
android:layout_margin="18dp"
|
||||||
android:contentDescription="Chat Global"
|
android:background="@android:color/transparent"
|
||||||
app:tint="@android:color/white"
|
android:src="@drawable/logo_chat"
|
||||||
app:backgroundTint="@color/button_color" />
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="Chat Global" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
38
app/src/main/res/layout/item_day_forecast.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:background="@android:color/transparent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDate"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="2025-12-29"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="13sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTempRange"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="18.1°C - 26.9°C"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvDayCondition"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Sunny"
|
||||||
|
android:textColor="#E6FFFFFF"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
41
app/src/main/res/layout/item_hour_forecast.xml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="110dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="14dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="6dp"
|
||||||
|
android:background="@android:color/transparent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHour"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="00:00"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHourTemp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="19.8°C"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvHourCondition"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Clear"
|
||||||
|
android:textColor="#E6FFFFFF"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="2dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@mipmap/ic_weatherhub"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@mipmap/ic_weatherhub"/>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 6.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_weatherhub.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
@ -6,4 +6,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.WeatherDemo" parent="Base.Theme.WeatherDemo" />
|
<style name="Theme.WeatherDemo" parent="Base.Theme.WeatherDemo" />
|
||||||
|
<style name="Theme.WeatherDemo.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||