diff --git a/app/src/main/assets/licenses/JetBrainsMonoNerdFont-OFL.txt b/app/src/main/assets/licenses/JetBrainsMonoNerdFont-OFL.txt new file mode 100644 index 0000000..b7bfff8 --- /dev/null +++ b/app/src/main/assets/licenses/JetBrainsMonoNerdFont-OFL.txt @@ -0,0 +1 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt index c27edcd..6b6d748 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/AppHelper.kt @@ -33,14 +33,18 @@ import com.github.droidworksstudio.launcher.R import com.github.droidworksstudio.launcher.accessibility.ActionService import com.github.droidworksstudio.launcher.data.dao.AppInfoDAO import com.github.droidworksstudio.launcher.data.entities.AppInfo +import com.github.droidworksstudio.launcher.helper.weather.MetForecastResponse import com.github.droidworksstudio.launcher.helper.weather.WeatherResponse import com.github.droidworksstudio.launcher.utils.Constants +import com.github.droidworksstudio.launcher.utils.MetApiService +import com.github.droidworksstudio.launcher.utils.MetSymbolMapper import com.github.droidworksstudio.launcher.utils.WeatherApiService import com.google.gson.Gson import com.google.gson.JsonSyntaxException import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import kotlinx.coroutines.flow.first +import kotlin.math.roundToInt import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.net.UnknownHostException @@ -552,4 +556,89 @@ class AppHelper @Inject constructor() { // Data class to hold cached weather data along with timestamp data class CachedWeatherData(val timestamp: Long, val weatherResponse: WeatherResponse) + + sealed class MetWeatherResult { + data class Success(val temperature: Int, val symbolCode: String) : MetWeatherResult() + data class Failure(val errorMessage: String) : MetWeatherResult() + } + + /** + * Fetches the current temperature and symbol from the MET/Yr API + * (api.met.no locationforecast). No API key needed - only a + * descriptive User-Agent header. Result is cached for 15 minutes. + */ + fun fetchMetWeather( + context: Context, + latitude: Float, + longitude: Float, + ): MetWeatherResult { + if (latitude == 0f && longitude == 0f) { + return MetWeatherResult.Failure("No location available") + } + + val cached = context.getMetWeatherFromCache() + if (cached?.let { + System.currentTimeMillis() - it.first < TimeUnit.MINUTES.toMillis(15) + } == true + ) { + return cached.second + } + + try { + val retrofit = Retrofit.Builder() + .baseUrl("https://api.met.no/weatherapi/locationforecast/2.0/") + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val service = retrofit.create(MetApiService::class.java) + val response = service.getCompact(latitude.toDouble(), longitude.toDouble()).execute() + if (response.isSuccessful) { + val body = response.body() + val timeseries = body?.properties?.timeseries + val first = timeseries?.firstOrNull() + if (first != null) { + val temperature = first.data.instant.details.airTemperature.roundToInt() + val symbolCode = first.data.next1Hours?.summary?.symbolCode + ?: first.data.next6Hours?.summary?.symbolCode + ?: "cloudy" + val result = MetWeatherResult.Success(temperature, symbolCode) + context.cacheMetWeather(result) + return result + } + return MetWeatherResult.Failure("Empty forecast") + } else { + return MetWeatherResult.Failure("MET API error: ${response.code()}") + } + } catch (e: UnknownHostException) { + return MetWeatherResult.Failure("Unknown host: api.met.no") + } catch (e: Exception) { + return MetWeatherResult.Failure(e.message ?: "MET fetch failed") + } + } + + /** + * Builds the home-screen weather text: nerd-font weather glyph followed + * by the temperature in Celsius, e.g. "\uE312 16°". + */ + fun buildCurrentWeatherText(temperature: Int, symbolCode: String): String { + return "${MetSymbolMapper.glyphFor(symbolCode)} $temperature°" + } + + private fun Context.cacheMetWeather(result: MetWeatherResult.Success) { + val sharedPreferences = getSharedPreferences(Constants.MET_WEATHER_PREFS, Context.MODE_PRIVATE) + sharedPreferences.edit() + .putLong(Constants.MET_WEATHER_TIMESTAMP, System.currentTimeMillis()) + .putInt(Constants.MET_WEATHER_TEMPERATURE, result.temperature) + .putString(Constants.MET_WEATHER_SYMBOL, result.symbolCode) + .apply() + } + + private fun Context.getMetWeatherFromCache(): Pair? { + val sharedPreferences = getSharedPreferences(Constants.MET_WEATHER_PREFS, Context.MODE_PRIVATE) + val timestamp = sharedPreferences.getLong(Constants.MET_WEATHER_TIMESTAMP, -1L) + if (timestamp == -1L) return null + val temperature = sharedPreferences.getInt(Constants.MET_WEATHER_TEMPERATURE, 0) + val symbol = sharedPreferences.getString(Constants.MET_WEATHER_SYMBOL, "cloudy") ?: "cloudy" + return timestamp to MetWeatherResult.Success(temperature, symbol) + } } diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt index fb59acf..b4f7a35 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/PreferenceHelper.kt @@ -202,6 +202,10 @@ class PreferenceHelper @Inject constructor(@ApplicationContext context: Context) get() = prefs.getBoolean(Constants.SHOW_NOTIFICATION_DOTS, false) set(value) = prefs.edit().putBoolean(Constants.SHOW_NOTIFICATION_DOTS, value).apply() + var showCurrentWeather: Boolean + get() = prefs.getBoolean(Constants.SHOW_CURRENT_WEATHER, false) + set(value) = prefs.edit().putBoolean(Constants.SHOW_CURRENT_WEATHER, value).apply() + var searchEngines: Constants.SearchEngines get() { return try { diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/MetForecastResponse.kt b/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/MetForecastResponse.kt new file mode 100644 index 0000000..b09b3ac --- /dev/null +++ b/app/src/main/java/com/github/droidworksstudio/launcher/helper/weather/MetForecastResponse.kt @@ -0,0 +1,46 @@ +package com.github.droidworksstudio.launcher.helper.weather + +import com.google.gson.annotations.SerializedName + +/** + * Locationforecast 2.0 compact response (api.met.no) - only the fields the + * home-screen current-weather element needs. + */ +data class MetForecastResponse( + val properties: MetProperties +) + +data class MetProperties( + val timeseries: List +) + +data class MetTimeSeries( + val time: String, + val data: MetData +) + +data class MetData( + val instant: MetInstant, + @SerializedName("next_1_hours") + val next1Hours: MetNextHours? = null, + @SerializedName("next_6_hours") + val next6Hours: MetNextHours? = null +) + +data class MetInstant( + val details: MetDetails +) + +data class MetDetails( + @SerializedName("air_temperature") + val airTemperature: Double = 0.0 +) + +data class MetNextHours( + val summary: MetSummary +) + +data class MetSummary( + @SerializedName("symbol_code") + val symbolCode: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/home/HomeFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/home/HomeFragment.kt index 96dfe36..996e70a 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/home/HomeFragment.kt @@ -22,6 +22,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatTextView import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -57,6 +58,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @@ -114,6 +116,12 @@ class HomeFragment : Fragment(), setupRecyclerView() observeSwipeTouchListener() observeUserInterfaceSettings() + + // Nerd-font weather glyphs (nf-weather-*) live in the bundled + // subset font; the system font here is JetBrainsMonoNerdFont but + // this guarantees rendering even if the launcher font is changed. + binding.currentWeather.typeface = + ResourcesCompat.getFont(requireContext(), R.font.jetbrains_mono_nf_weather) } @SuppressLint("ClickableViewAccessibility") @@ -323,6 +331,17 @@ class HomeFragment : Fragment(), ) } + preferenceViewModel.showCurrentWeatherLiveData.observe(viewLifecycleOwner) { + appHelper.updateUI( + binding.currentWeather, + preferenceHelper.homeDailyWordAlignment, + preferenceHelper.dailyWordColor, + preferenceHelper.dailyWordTextSize, + it + ) + if (it) loadCurrentWeather() + } + binding.apply { mainView.hideKeyboard() @@ -343,6 +362,41 @@ class HomeFragment : Fragment(), } } + /** + * Fetches the current temperature from the MET/Yr API and shows it as a + * nerd-font weather glyph plus Celsius value, e.g. "\uE312 16°". + * Formatted like the daily word (same alignment/color/size settings). + */ + private fun loadCurrentWeather() { + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + val sharedPreferences = + requireContext().getSharedPreferences(Constants.WEATHER_PREFS, Context.MODE_PRIVATE) + val latitude = sharedPreferences.getFloat(Constants.LATITUDE, 0f) + val longitude = sharedPreferences.getFloat(Constants.LONGITUDE, 0f) + val result = appHelper.fetchMetWeather(requireContext(), latitude, longitude) + + withContext(Dispatchers.Main) { + when (result) { + is AppHelper.MetWeatherResult.Success -> { + binding.currentWeather.text = appHelper.buildCurrentWeatherText( + result.temperature, + result.symbolCode + ) + binding.currentWeather.visibility = View.VISIBLE + } + + is AppHelper.MetWeatherResult.Failure -> { + // No data (offline, no location): keep any cached text, + // otherwise hide the element. + if (binding.currentWeather.text.isNullOrEmpty()) { + binding.currentWeather.visibility = View.GONE + } + } + } + } + } + } + private fun observeBioAuthCheck(appInfo: AppInfo) { if (!appInfo.lock) context.launchApp(appInfo) @@ -607,6 +661,7 @@ class HomeFragment : Fragment(), observeUserInterfaceSettings() observeFavoriteAppList() observeNotificationBadges() + if (preferenceHelper.showCurrentWeather) loadCurrentWeather() } override fun onAppClicked(appInfo: AppInfo) { diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsLookFeelFragment.kt b/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsLookFeelFragment.kt index 8811d60..21a6151 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsLookFeelFragment.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/ui/settings/SettingsLookFeelFragment.kt @@ -95,6 +95,7 @@ class SettingsLookFeelFragment : Fragment(), alarmClockSwitchCompat.isChecked = preferenceHelper.showAlarmClock dailyWordSwitchCompat.isChecked = preferenceHelper.showDailyWord appIconsSwitchCompat.isChecked = preferenceHelper.showAppIcon + currentWeatherSwitchCompat.isChecked = preferenceHelper.showCurrentWeather } } @@ -188,6 +189,12 @@ class SettingsLookFeelFragment : Fragment(), val feedbackType = if (isChecked) "on" else "off" appHelper.triggerHapticFeedback(context, feedbackType) } + + currentWeatherSwitchCompat.setOnCheckedChangeListener { _, isChecked -> + preferenceViewModel.setShowCurrentWeather(isChecked) + val feedbackType = if (isChecked) "on" else "off" + appHelper.triggerHapticFeedback(context, feedbackType) + } } } diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt b/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt index 81d8a44..b5d4e0b 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/utils/Constants.kt @@ -20,6 +20,10 @@ object Constants { const val WIDGET_BATTERY = "WIDGET_BATTERY" const val WEATHER_PREFS = "EasyWeather.pref" + const val MET_WEATHER_PREFS = "met_weather_prefs" + const val MET_WEATHER_TIMESTAMP = "MET_WEATHER_TIMESTAMP" + const val MET_WEATHER_TEMPERATURE = "MET_WEATHER_TEMPERATURE" + const val MET_WEATHER_SYMBOL = "MET_WEATHER_SYMBOL" const val WEATHER_RESPONSE = "WEATHER_RESPONSE" const val WEATHER_UNITS = "WEATHER_UNITS" const val LATITUDE = "LATITUDE" @@ -71,6 +75,7 @@ object Constants { const val TOGGLE_SETTING_LOCK = "TOGGLE_SETTING_LOCK" const val DISABLE_ANIMATIONS = "DISABLE_ANIMATIONS" const val SHOW_NOTIFICATION_DOTS = "SHOW_NOTIFICATION_DOTS" + const val SHOW_CURRENT_WEATHER = "SHOW_CURRENT_WEATHER" const val HOME_DATE_ALIGNMENT = "HOME_DATE_ALIGNMENT" const val HOME_TIME_ALIGNMENT = "HOME_TIME_ALIGNMENT" diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetApiService.kt b/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetApiService.kt new file mode 100644 index 0000000..5012e63 --- /dev/null +++ b/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetApiService.kt @@ -0,0 +1,19 @@ +package com.github.droidworksstudio.launcher.utils + +import com.github.droidworksstudio.launcher.helper.weather.MetForecastResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Query + +interface MetApiService { + + @Headers( + "User-Agent: app.easy.launcher (https://gitea.haugesenspil.dk/jonas/EasyLauncher)" + ) + @GET("compact") + fun getCompact( + @Query("lat") latitude: Double, + @Query("lon") longitude: Double + ): Call +} \ No newline at end of file diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetSymbolMapper.kt b/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetSymbolMapper.kt new file mode 100644 index 0000000..f253d5b --- /dev/null +++ b/app/src/main/java/com/github/droidworksstudio/launcher/utils/MetSymbolMapper.kt @@ -0,0 +1,57 @@ +package com.github.droidworksstudio.launcher.utils + +/** + * Maps MET/Yr symbol codes to Nerd Font weather glyphs + * (nf-weather-* codepoints from the Weather Icons font embedded in + * JetBrainsMonoNerdFont). Rendered with R.font.jetbrains_mono_nf_weather. + */ +object MetSymbolMapper { + + /** + * Returns the nf-weather glyph char for a MET `symbol_code` + * (e.g. "partlycloudy_day"). Unknown codes fall back to a cloud. + */ + fun glyphFor(symbolCode: String): Char { + val normalized = symbolCode.removeSuffix("_polartwilight") + val night = normalized.endsWith("_night") + val base = normalized + .removeSuffix("_day") + .removeSuffix("_night") + + return when (base) { + "clearsky", "fair" -> if (night) '\uE32B' else '\uE30D' + "partlycloudy" -> if (night) '\uE379' else '\uE302' + "cloudy" -> '\uE312' + "fog" -> '\uE313' + "lightrain" -> '\uE31B' + "rain" -> '\uE318' + "heavyrain" -> '\uE319' + "lightrainshowers" -> if (night) '\uE328' else '\uE30B' + "rainshowers" -> if (night) '\uE326' else '\uE309' + "heavyrainshowers" -> if (night) '\uE324' else '\uE307' + "lightrainandthunder" -> '\uE315' + "rainandthunder" -> '\uE31D' + "heavyrainandthunder" -> '\uE31C' + "lightrainshowersandthunder" -> if (night) '\uE322' else '\uE305' + "rainshowersandthunder" -> if (night) '\uE32A' else '\uE30F' + "heavyrainshowersandthunder" -> if (night) '\uE329' else '\uE30E' + "sleet" -> '\uE316' + "lightssleetshowers", "sleetshowers" -> if (night) '\uE323' else '\uE306' + "heavysleetshowers" -> if (night) '\uE364' else '\uE362' + "sleetandthunder", + "lightsleetshowersandthunder", + "sleetshowersandthunder", + -> if (night) '\uE364' else '\uE362' + "lightssnow", "lightsnow", "snow" -> '\uE31A' + "heavysnow" -> '\uE35E' + "lightssnowshowers", "snowshowers" -> if (night) '\uE327' else '\uE30A' + "heavysnowshowers" -> if (night) '\uE361' else '\uE35F' + "snowandthunder", + "snowshowersandthunder", + -> if (night) '\uE367' else '\uE365' + "thunder" -> '\uE31D' + "wind" -> '\uE34B' + else -> '\uE312' + } + } +} diff --git a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt index 630d26e..45ab7c7 100644 --- a/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt +++ b/app/src/main/java/com/github/droidworksstudio/launcher/viewmodel/PreferenceViewModel.kt @@ -49,7 +49,8 @@ class PreferenceViewModel @Inject constructor( private val autoKeyboardLiveData: MutableLiveData = MutableLiveData() private val lockSettingsLiveData: MutableLiveData = MutableLiveData() private val disableAnimationsLiveData: MutableLiveData = MutableLiveData() - private val showNotificationDotsLiveData: MutableLiveData = MutableLiveData() + val showNotificationDotsLiveData: MutableLiveData = MutableLiveData() + val showCurrentWeatherLiveData: MutableLiveData = MutableLiveData() private val appGroupPaddingSizeLiveData: MutableLiveData = MutableLiveData() private val appPaddingSizeLiveData: MutableLiveData = MutableLiveData() @@ -303,6 +304,11 @@ class PreferenceViewModel @Inject constructor( showNotificationDotsLiveData.postValue((preferenceHelper.showNotificationDots)) } + fun setShowCurrentWeather(showCurrentWeather: Boolean) { + preferenceHelper.showCurrentWeather = showCurrentWeather + showCurrentWeatherLiveData.postValue((preferenceHelper.showCurrentWeather)) + } + fun setAppLanguage(appLanguage: Constants.Language) { preferenceHelper.appLanguage = appLanguage appLanguageLiveData.postValue((preferenceHelper.appLanguage)) diff --git a/app/src/main/res/font/jetbrains_mono_nf_weather.ttf b/app/src/main/res/font/jetbrains_mono_nf_weather.ttf new file mode 100644 index 0000000..c49e31e Binary files /dev/null and b/app/src/main/res/font/jetbrains_mono_nf_weather.ttf differ diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index d2f5b96..41679b5 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -78,6 +78,14 @@ android:textSize="32sp" android:visibility="gone" /> + + + + + + + + + + Show Alarm Clock Show Daily Word Show App Icons + Show Current Weather Auto Show Keyboard Auto Open Last App Home Alignment Bottom