Project
8
.idea/deploymentTargetSelector.xml
generated
@ -4,14 +4,6 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<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>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
1
.idea/misc.xml
generated
@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<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" />
|
||||
|
||||
<application
|
||||
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/ic_launcher_foreground"
|
||||
android:roundIcon="@drawable/ic_launcher_foreground"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.WeatherDemo"
|
||||
@ -33,7 +34,10 @@
|
||||
<!-- WAJIB: ChatActivity -->
|
||||
<activity
|
||||
android:name=".ui.ChatActivity"
|
||||
android:exported="true" />
|
||||
android:theme="@style/Theme.WeatherDemo.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden"/>
|
||||
|
||||
|
||||
|
||||
</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
|
||||
|
||||
import com.example.weatherdemo.data.models.WeatherForecastResponse
|
||||
import com.example.weatherdemo.data.models.WeatherResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
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
|
||||
*/
|
||||
interface WeatherApiService {
|
||||
|
||||
/**
|
||||
* 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")
|
||||
suspend fun getCurrentWeather(
|
||||
@ -24,4 +21,17 @@ interface WeatherApiService {
|
||||
@Query("q") query: String,
|
||||
@Query("aqi") aqi: String = "yes"
|
||||
): 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
|
||||
|
||||
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 retrofit2.Response
|
||||
|
||||
@ -12,23 +12,24 @@ import retrofit2.Response
|
||||
class WeatherRepository(private val apiService: WeatherApiService) {
|
||||
|
||||
/**
|
||||
* Fetches weather data from API and returns a Result wrapper
|
||||
* @param location The location to get weather for
|
||||
* @return Result object containing either success or error
|
||||
* Fetches forecast weather data from API and returns a Result wrapper.
|
||||
* This uses WeatherAPI "forecast.json" so we can show hourly/daily prediction on main page.
|
||||
*
|
||||
* @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 {
|
||||
// Make API call
|
||||
val response: Response<WeatherResponse> = apiService.getCurrentWeather(
|
||||
val response: Response<WeatherForecastResponse> = apiService.getForecastWeather(
|
||||
key = "822615b3cef1437bb0202739251712", // API key
|
||||
query = location
|
||||
query = location,
|
||||
days = days
|
||||
)
|
||||
|
||||
// Check if response is successful
|
||||
if (response.isSuccessful) {
|
||||
val weatherResponse = response.body()
|
||||
if (weatherResponse != null) {
|
||||
Result.Success(weatherResponse)
|
||||
val body = response.body()
|
||||
if (body != null) {
|
||||
Result.Success(body)
|
||||
} else {
|
||||
Result.Error(Exception("Empty response body"))
|
||||
}
|
||||
|
||||
@ -3,8 +3,12 @@ package com.example.weatherdemo.ui
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
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.RecyclerView
|
||||
import com.example.weatherdemo.R
|
||||
@ -20,64 +24,67 @@ class ChatActivity : AppCompatActivity() {
|
||||
private lateinit var btnSend: Button
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var chatAdapter: ChatAdapter
|
||||
private lateinit var username: String // username user aktif
|
||||
private lateinit var username: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// ✅ penting: kita handle insets sendiri biar semua HP aman
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
btnSend = findViewById(R.id.btnSend)
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
|
||||
val layoutManager = LinearLayoutManager(this).apply {
|
||||
stackFromEnd = true
|
||||
}
|
||||
recyclerView.layoutManager = layoutManager
|
||||
|
||||
// 🔥 WAJIB kirim username ke ChatAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
|
||||
chatAdapter = ChatAdapter(username)
|
||||
recyclerView.adapter = chatAdapter
|
||||
|
||||
btnSend.setOnClickListener {
|
||||
val messageText = etMessage.text.toString().trim()
|
||||
|
||||
if (messageText.isNotEmpty()) {
|
||||
|
||||
val time = SimpleDateFormat(
|
||||
"HH:mm",
|
||||
Locale.getDefault()
|
||||
).format(Date())
|
||||
|
||||
FirebaseUtils.sendMessage(
|
||||
username = username,
|
||||
message = messageText,
|
||||
time = time
|
||||
)
|
||||
|
||||
val time = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date())
|
||||
FirebaseUtils.sendMessage(username = username, message = messageText, time = time)
|
||||
etMessage.setText("")
|
||||
}
|
||||
}
|
||||
|
||||
// biar insets langsung diterapkan
|
||||
ViewCompat.requestApplyInsets(root)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
FirebaseUtils.getMessages { messages ->
|
||||
chatAdapter.submitList(messages)
|
||||
|
||||
if (messages.isNotEmpty()) {
|
||||
recyclerView.post {
|
||||
recyclerView.scrollToPosition(messages.size - 1)
|
||||
}
|
||||
recyclerView.post { recyclerView.scrollToPosition(messages.size - 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,38 +2,33 @@ package com.example.weatherdemo.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.example.weatherdemo.R
|
||||
import com.google.android.material.button.MaterialButton
|
||||
|
||||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var etUsername: EditText
|
||||
private lateinit var btnLogin: Button
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_login)
|
||||
|
||||
etUsername = findViewById(R.id.etUsername)
|
||||
btnLogin = findViewById(R.id.btnLogin)
|
||||
val etUsername = findViewById<EditText>(R.id.etUsername)
|
||||
val btnLogin = findViewById<MaterialButton>(R.id.btnLogin)
|
||||
|
||||
btnLogin.setOnClickListener {
|
||||
val username = etUsername.text.toString().trim()
|
||||
|
||||
if (username.isNotEmpty()) {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
|
||||
// ✅ FIX: key KONSISTEN
|
||||
intent.putExtra("username", username)
|
||||
|
||||
startActivity(intent)
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(this, "Please enter a username", Toast.LENGTH_SHORT).show()
|
||||
if (username.isEmpty()) {
|
||||
Toast.makeText(this, "Masukkan username dulu", Toast.LENGTH_SHORT).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
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.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.example.weatherdemo.R
|
||||
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.ui.adapter.DailyForecastAdapter
|
||||
import com.example.weatherdemo.ui.adapter.HourlyForecastAdapter
|
||||
import com.example.weatherdemo.viewmodel.WeatherViewModel
|
||||
import com.example.weatherdemo.viewmodel.WeatherViewModelFactory
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@ -30,26 +32,43 @@ class MainActivity : AppCompatActivity() {
|
||||
WeatherViewModelFactory(WeatherRepository(RetrofitInstance.apiService))
|
||||
}
|
||||
|
||||
private lateinit var hourlyAdapter: HourlyForecastAdapter
|
||||
private lateinit var dailyAdapter: DailyForecastAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// ✅ FIX DI SINI SAJA
|
||||
val username = intent.getStringExtra("username") ?: "Guest"
|
||||
findViewById<TextView>(R.id.tvWelcomeMessage).text = "Welcome, $username!"
|
||||
|
||||
val tvWelcomeMessage = findViewById<TextView>(R.id.tvWelcomeMessage)
|
||||
tvWelcomeMessage.text = "Welcome, $username!"
|
||||
|
||||
// FAB chat → kirim username ke ChatActivity
|
||||
findViewById<FloatingActionButton>(R.id.fabChat).setOnClickListener {
|
||||
// ✅ CHAT GLOBAL → ImageButton (BUKAN FloatingActionButton)
|
||||
findViewById<ImageButton>(R.id.fabChat).setOnClickListener {
|
||||
val i = Intent(this, ChatActivity::class.java)
|
||||
i.putExtra("username", username)
|
||||
startActivity(i)
|
||||
}
|
||||
|
||||
initForecastLists()
|
||||
initViews()
|
||||
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() {
|
||||
@ -60,7 +79,7 @@ class MainActivity : AppCompatActivity() {
|
||||
val location = etSearch.text.toString().trim()
|
||||
if (location.isNotEmpty()) {
|
||||
hideKeyboard()
|
||||
viewModel.fetchWeatherData(location)
|
||||
viewModel.fetchForecast(location, days = 3)
|
||||
} else {
|
||||
Toast.makeText(this, "Please enter a location", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@ -68,78 +87,73 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.weatherData.observe(this, Observer { weatherData ->
|
||||
weatherData?.let { updateWeatherUI(it) }
|
||||
})
|
||||
viewModel.forecastData.observe(this) { forecast ->
|
||||
forecast?.let { updateWeatherUI(it) }
|
||||
}
|
||||
|
||||
viewModel.isLoading.observe(this, Observer { isLoading ->
|
||||
viewModel.isLoading.observe(this) { isLoading ->
|
||||
findViewById<ProgressBar>(R.id.progressBar).visibility =
|
||||
if (isLoading) View.VISIBLE else View.GONE
|
||||
})
|
||||
}
|
||||
|
||||
viewModel.errorMessage.observe(this, Observer { errorMessage ->
|
||||
val tvError = findViewById<TextView>(R.id.tvError)
|
||||
if (errorMessage.isNotEmpty()) {
|
||||
tvError.text = errorMessage
|
||||
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))
|
||||
viewModel.errorMessage.observe(this) { errorMessage ->
|
||||
findViewById<TextView>(R.id.tvError).apply {
|
||||
text = errorMessage
|
||||
visibility = if (errorMessage.isNullOrBlank()) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
val view = this.currentFocus
|
||||
view?.let {
|
||||
currentFocus?.let {
|
||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
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.ViewModel
|
||||
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 kotlinx.coroutines.launch
|
||||
import com.example.weatherdemo.utils.Result
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
// LiveData for weather information - observed by UI
|
||||
private val _weatherData = MutableLiveData<WeatherResponse>()
|
||||
val weatherData: LiveData<WeatherResponse> = _weatherData
|
||||
// LiveData for forecast response (current + hourly + daily)
|
||||
private val _forecastData = MutableLiveData<WeatherForecastResponse>()
|
||||
val forecastData: LiveData<WeatherForecastResponse> = _forecastData
|
||||
|
||||
// LiveData for loading state - to show/hide progress bar
|
||||
private val _isLoading = MutableLiveData<Boolean>()
|
||||
@ -29,17 +29,15 @@ class WeatherViewModel(private val repository: WeatherRepository) : ViewModel()
|
||||
val errorMessage: LiveData<String> = _errorMessage
|
||||
|
||||
/**
|
||||
* Fetches weather data for a given location
|
||||
* Uses coroutines for background operations
|
||||
* @param location The city name to search for
|
||||
* Fetches forecast data for a given location
|
||||
*/
|
||||
fun fetchWeatherData(location: String) {
|
||||
fun fetchForecast(location: String, days: Int = 3) {
|
||||
_isLoading.value = true
|
||||
|
||||
viewModelScope.launch {
|
||||
when (val result = repository.getWeatherData(location)) {
|
||||
when (val result = repository.getWeatherForecast(location, days)) {
|
||||
is Result.Success -> {
|
||||
_weatherData.value = result.data
|
||||
_forecastData.value = result.data
|
||||
_errorMessage.value = ""
|
||||
}
|
||||
is Result.Error -> {
|
||||
|
||||
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"?>
|
||||
<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:id="@+id/chatRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@drawable/background_gradient">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- TOOLBAR + TOMBOL BACK -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbarChat"
|
||||
android:layout_width="match_parent"
|
||||
<!-- TOP BAR -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/topBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/button_color"
|
||||
app:title="Global Chat"
|
||||
app:titleTextColor="@android:color/white"
|
||||
app:navigationIcon="@android:drawable/ic_media_previous" />
|
||||
android:background="#2F55F4"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
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
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:scrollbars="vertical"
|
||||
android:paddingBottom="8dp"
|
||||
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 -->
|
||||
<EditText
|
||||
android:id="@+id/etMessage"
|
||||
android:layout_width="match_parent"
|
||||
<!-- INPUT BAR -->
|
||||
<LinearLayout
|
||||
android:id="@+id/inputBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Type a message"
|
||||
android:inputType="text"
|
||||
android:padding="10dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@android:color/white" />
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:background="#F2F2F2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
<!-- Send -->
|
||||
<Button
|
||||
android:id="@+id/btnSend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Send"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:background="@color/button_color" />
|
||||
<EditText
|
||||
android:id="@+id/etMessage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@android:color/black"
|
||||
android:textColorHint="#88000000"
|
||||
android:hint="Type a message"
|
||||
android:background="@android:color/white"
|
||||
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"?>
|
||||
<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_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/background_gradient">
|
||||
android:background="@android:color/white">
|
||||
|
||||
<!-- Username Input Field -->
|
||||
<EditText
|
||||
android:id="@+id/etUsername"
|
||||
android:layout_width="match_parent"
|
||||
<ImageView
|
||||
android:id="@+id/ivLogo"
|
||||
android:layout_width="320dp"
|
||||
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:hint="Enter username"
|
||||
android:textColor="@color/black"
|
||||
android:textColorHint="@color/gray"
|
||||
android:background="@android:color/white"
|
||||
android:padding="12dp" />
|
||||
android:text="WeatherHub"
|
||||
android:textStyle="bold"
|
||||
android:textSize="22sp"
|
||||
android:textColor="@android:color/black"
|
||||
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 -->
|
||||
<Button
|
||||
<!-- INPUT = ukuran sama dengan 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_marginStart="44dp"
|
||||
android:layout_marginEnd="44dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Login"
|
||||
android:layout_marginTop="20dp" />
|
||||
</LinearLayout>
|
||||
android:textAllCaps="false"
|
||||
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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/background_gradient">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<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
|
||||
android:layout_width="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
|
||||
android:id="@+id/mainLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/solid_background_color">
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Header -->
|
||||
<androidx.cardview.widget.CardView
|
||||
<!-- Welcome -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
app:cardCornerRadius="18dp"
|
||||
app:cardElevation="8dp"
|
||||
app:cardBackgroundColor="@color/welcome_card_background">
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardBackgroundColor="#2F55F4">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWelcomeMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="Welcome!"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Search -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="8dp"
|
||||
app:cardBackgroundColor="@color/card_background">
|
||||
android:layout_marginBottom="14dp"
|
||||
app:cardCornerRadius="18dp"
|
||||
app:cardElevation="10dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="#22FFFFFF"
|
||||
app:cardBackgroundColor="#55FFFFFF">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
android:padding="14dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:layout_weight="1"
|
||||
android:hint="Enter city name"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/hint_color"
|
||||
android:background="@android:color/transparent" />
|
||||
android:textColor="@android:color/white"
|
||||
android:textColorHint="#CCFFFFFF"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSearch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="44dp"
|
||||
android:text="Search"
|
||||
android:textColor="@color/white"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@android:color/white"
|
||||
app:cornerRadius="0dp"
|
||||
app:icon="@drawable/ic_search"
|
||||
android:backgroundTint="@color/button_color" />
|
||||
app:iconPadding="8dp"
|
||||
app:backgroundTint="#2F55F4" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Progress -->
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:layout_marginBottom="10dp" />
|
||||
|
||||
<!-- Error -->
|
||||
<TextView
|
||||
android:id="@+id/tvError"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/error_color"
|
||||
android:textColor="#FFFF4444"
|
||||
android:gravity="center"
|
||||
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
|
||||
android:id="@+id/weatherCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="24dp"
|
||||
app:cardElevation="12dp"
|
||||
app:cardBackgroundColor="@color/weather_card_background">
|
||||
app:cardCornerRadius="22dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardBackgroundColor="@android:color/transparent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLocation"
|
||||
<!-- BOX 1 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Location"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="8dp" />
|
||||
app:cardCornerRadius="18dp"
|
||||
app:cardElevation="10dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="#22FFFFFF"
|
||||
app:cardBackgroundColor="#26FFFFFF">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTemperature"
|
||||
<LinearLayout
|
||||
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_height="wrap_content"
|
||||
android:text="--°C"
|
||||
android:textSize="48sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
app:cardCornerRadius="18dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="#22FFFFFF"
|
||||
app:cardBackgroundColor="#26FFFFFF">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCondition"
|
||||
<LinearLayout
|
||||
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_height="wrap_content"
|
||||
android:text="Condition"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
app:cardCornerRadius="18dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardUseCompatPadding="false"
|
||||
app:strokeWidth="1dp"
|
||||
app:strokeColor="#22FFFFFF"
|
||||
app:cardBackgroundColor="#26FFFFFF">
|
||||
|
||||
<GridLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="2">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Feels Like:"
|
||||
android:textColor="@color/label_color"
|
||||
android:textSize="14sp" />
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFeelsLike"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="--°C"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:layout_gravity="end" />
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Prediksi per Hari (3 Hari)"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
android:textSize="15sp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Humidity:"
|
||||
android:textColor="@color/label_color"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<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>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvDailyForecast"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="never" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@ -199,15 +328,16 @@
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<!-- FAB CHAT GLOBAL -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
<!-- Chat Global: pakai gambar dari drawable -->
|
||||
<ImageButton
|
||||
android:id="@+id/fabChat"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="66dp"
|
||||
android:layout_height="66dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="20dp"
|
||||
android:contentDescription="Chat Global"
|
||||
app:tint="@android:color/white"
|
||||
app:backgroundTint="@color/button_color" />
|
||||
android:layout_margin="18dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/logo_chat"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="Chat Global" />
|
||||
|
||||
</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"?>
|
||||
<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"/>
|
||||
</adaptive-icon>
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"/>
|
||||
</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 name="Theme.WeatherDemo" parent="Base.Theme.WeatherDemo" />
|
||||
<style name="Theme.WeatherDemo.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
</style>
|
||||
|
||||
</resources>
|
||||