Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ dependencies {
implementation libs.disk.lru.cache
implementation libs.markwon
implementation libs.kizzyrpc

implementation libs.konfetti.xml
implementation libs.konfetti.core
implementation libs.conscrypt.android
implementation libs.sentry.android

Expand Down
3 changes: 0 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,6 @@
<activity
android:name="io.github.landwarderer.futon.alternatives.ui.AlternativesActivity"
android:label="@string/alternatives" />
<activity
android:name="io.github.landwarderer.futon.settings.about.AppUpdateActivity"
android:label="@string/app_update_available" />
<activity
android:name="io.github.landwarderer.futon.tracker.ui.debug.TrackerDebugActivity"
android:label="@string/tracker_debug_info" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,29 @@ package io.github.landwarderer.futon.core.github

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.landwarderer.futon.BuildConfig
import io.github.landwarderer.futon.R
import io.github.landwarderer.futon.core.network.BaseHttpClient
import io.github.landwarderer.futon.core.prefs.AppSettings
import io.github.landwarderer.futon.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import javax.inject.Inject
import javax.inject.Singleton

private const val BUILD_TYPE_RELEASE = "release"

@Singleton
class AppUpdateRepository @Inject constructor(
private val settings: AppSettings,
@BaseHttpClient private val okHttp: OkHttpClient,
@ApplicationContext context: Context,
) {
// TODO("Fix update checking.")
private val availableUpdate = MutableStateFlow<AppVersion?>(null)
private val latestReleaseUrl = buildString {
append("https://api.github.com/repos/")
append(context.getString(R.string.github_updates_repo))
append("/releases/latest")
}

private val changelogUrl = buildString {
append("https://raw.githubusercontent.com/")
append(context.getString(R.string.github_updates_repo))
append("/refs/heads/devel/CHANGELOG.md")
}

val isUpdateAvailable: Boolean
get() = availableUpdate.value != null

fun observeAvailableUpdate() = availableUpdate.asStateFlow()

suspend fun fetchUpdate(): AppVersion? = withContext(Dispatchers.IO) {
runCatchingCancellable {
val request = Request.Builder()
.get()
.url(latestReleaseUrl)
.build()
val response = okHttp.newCall(request).await()
val json = JSONObject(response.body?.string() ?: "{}")

val currentVersion = VersionId(BuildConfig.VERSION_NAME)
val releaseVersion = VersionId(json.getString("tag_name").removePrefix("v"))

// Only return update if there's a newer version available
if (releaseVersion <= currentVersion) {
return@runCatchingCancellable null
}

AppVersion(
id = json.getLong("id"),
url = json.getString("html_url"),
name = json.getString("name").removePrefix("v"),
apkSize = 0L, // No longer downloading, so size not needed
apkUrl = "", // No longer downloading
description = json.getString("body"),
)
}.onFailure {
it.printStackTraceDebug("AppUpdateRepository::fetchUpdate")
}.onSuccess {
availableUpdate.value = it
}.getOrNull()
}

suspend fun fetchChangelog(): String? = withContext(Dispatchers.IO) {
runCatchingCancellable {
val request = Request.Builder()
Expand All @@ -89,9 +36,4 @@ class AppUpdateRepository @Inject constructor(
it.printStackTraceDebug("AppUpdateRepository::fetchChangelog")
}.getOrNull()
}

@Suppress("KotlinConstantConditions")
suspend fun isUpdateSupported(): Boolean {
return true // Updates are always available now (just checking for newer version)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ import io.github.landwarderer.futon.search.domain.SearchKind
import io.github.landwarderer.futon.search.ui.MangaListActivity
import io.github.landwarderer.futon.search.ui.multi.SearchActivity
import io.github.landwarderer.futon.settings.SettingsActivity
import io.github.landwarderer.futon.settings.about.AppUpdateActivity
import io.github.landwarderer.futon.settings.override.OverrideConfigActivity
import io.github.landwarderer.futon.settings.reader.ReaderTapGridConfigActivity
import io.github.landwarderer.futon.settings.sources.auth.SourceAuthActivity
Expand Down Expand Up @@ -200,7 +199,6 @@ class AppRouter private constructor(

fun openBookmarks() = startActivity(AllBookmarksActivity::class.java)

fun openAppUpdate() = startActivity(AppUpdateActivity::class.java)

fun openSuggestions() {
startActivity(suggestionsIntent(contextOrNull() ?: return))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.landwarderer.futon.R
import io.github.landwarderer.futon.core.github.AppUpdateRepository
import io.github.landwarderer.futon.core.nav.AppRouter
import io.github.landwarderer.futon.core.nav.router
import io.github.landwarderer.futon.core.ui.AlertDialogFragment
Expand All @@ -20,16 +19,12 @@ import io.github.landwarderer.futon.core.util.ext.report
import io.github.landwarderer.futon.core.util.ext.requireSerializable
import io.github.landwarderer.futon.core.util.ext.setTextAndVisible
import io.github.landwarderer.futon.databinding.DialogErrorDetailsBinding
import javax.inject.Inject

@AndroidEntryPoint
class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>(), View.OnClickListener {

private lateinit var exception: Throwable

@Inject
lateinit var appUpdateRepository: AppUpdateRepository

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = requireArguments()
Expand All @@ -48,9 +43,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>(), Vie
binding.buttonBrowser.isVisible = isUrlAvailable
binding.textViewBrowser.isVisible = isUrlAvailable
binding.textViewDescription.setTextAndVisible(
if (appUpdateRepository.isUpdateAvailable) {
R.string.error_disclaimer_app_outdated
} else if (exception.isReportable()) {
if (exception.isReportable()) {
R.string.error_disclaimer_report
} else {
0
Expand All @@ -67,12 +60,7 @@ class ErrorDetailsDialog : AlertDialogFragment<DialogErrorDetailsBinding>(), Vie
.setNeutralButton(androidx.preference.R.string.copy) { _, _ ->
context?.copyToClipboard(getString(R.string.error), exception.stackTraceToString())
}
if (appUpdateRepository.isUpdateAvailable) {
builder.setPositiveButton(R.string.update) { _, _ ->
router.openAppUpdate()
dismiss()
}
} else if (exception.isReportable()) {
if (exception.isReportable()) {
builder.setPositiveButton(R.string.report) { _, _ ->
exception.report(silent = true)
dismiss()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
viewModel.isLoading.observe(this@MainActivity, this@MainActivity::onLoadingStateChanged)
viewModel.isResumeEnabled.observe(this@MainActivity, this@MainActivity::onResumeEnabledChanged)
viewModel.feedCounter.observe(this@MainActivity, ::onFeedCounterChanged)
viewModel.appUpdate.observe(this@MainActivity, MenuInvalidator(this@MainActivity))
viewModel.onFirstStart.observeEvent(this@MainActivity) { router.showWelcomeSheet() }
viewModel.isBottomNavPinned.observe(this@MainActivity, ::setNavbarPinned)
searchSuggestionViewModel.isIncognitoModeEnabled.observe(this@MainActivity, this@MainActivity::onIncognitoModeChanged)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ class MainMenuProvider(
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_incognito)?.isChecked =
viewModel.isIncognitoModeEnabled.value
val hasAppUpdate = viewModel.appUpdate.value != null
menu.findItem(R.id.action_app_update)?.isVisible = hasAppUpdate
menu.findItem(R.id.action_app_update)?.isVisible = false
}

override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
Expand All @@ -34,11 +33,6 @@ class MainMenuProvider(
true
}

R.id.action_app_update -> {
router.openAppUpdate()
true
}

R.id.action_downloads -> {
router.openDownloads()
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import io.github.landwarderer.futon.core.exceptions.EmptyHistoryException
import io.github.landwarderer.futon.core.github.AppUpdateRepository
import io.github.landwarderer.futon.core.prefs.AppSettings
import io.github.landwarderer.futon.core.prefs.observeAsFlow
import io.github.landwarderer.futon.core.prefs.observeAsStateFlow
Expand All @@ -25,7 +24,6 @@ import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val appUpdateRepository: AppUpdateRepository,
trackingRepository: TrackingRepository,
private val settings: AppSettings,
readingResumeEnabledUseCase: ReadingResumeEnabledUseCase,
Expand All @@ -43,7 +41,6 @@ class MainViewModel @Inject constructor(
initialValue = false,
)

val appUpdate = appUpdateRepository.observeAvailableUpdate()

val feedCounter = trackingRepository.observeUnreadUpdatesCount()
.withErrorHandling()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,83 @@
package io.github.landwarderer.futon.settings.about

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import io.github.landwarderer.futon.BuildConfig
import io.github.landwarderer.futon.R
import io.github.landwarderer.futon.core.github.AppVersion
import io.github.landwarderer.futon.core.github.VersionId
import io.github.landwarderer.futon.core.github.isStable
import io.github.landwarderer.futon.core.nav.router
import io.github.landwarderer.futon.core.prefs.AppSettings
import io.github.landwarderer.futon.core.ui.BasePreferenceFragment
import io.github.landwarderer.futon.core.util.ext.observe
import io.github.landwarderer.futon.core.util.ext.observeEvent
import nl.dionsegijn.konfetti.core.models.Shape
import nl.dionsegijn.konfetti.xml.KonfettiView
import nl.dionsegijn.konfetti.xml.image.DrawableImage
import kotlin.random.Random

@AndroidEntryPoint
class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {

private val viewModel by viewModels<AboutSettingsViewModel>()
private var versionClickCount = 0
private lateinit var konfettiView: KonfettiView

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_about)
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.run {
title = getString(R.string.app_version, BuildConfig.VERSION_NAME)
}
findPreference<Preference>(AppSettings.KEY_LINK_TELEGRAM)?.isVisible = false
findPreference<SwitchPreferenceCompat>(AppSettings.KEY_UPDATES_UNSTABLE)?.run {
isEnabled = VersionId(BuildConfig.VERSION_NAME).isStable
if (!isEnabled) isChecked = true
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val list = super.onCreateView(inflater, container, savedInstanceState)
konfettiView = KonfettiView(requireContext()).apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Ensure it doesn't consume clicks
isClickable = false
isFocusable = false
}
return FrameLayout(requireContext()).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
addView(list)
addView(konfettiView)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
combine(viewModel.isUpdateSupported, viewModel.isLoading, ::Pair)
.observe(viewLifecycleOwner) { (isUpdateSupported, isLoading) ->
findPreference<Preference>(AppSettings.KEY_UPDATES_UNSTABLE)?.isVisible = isUpdateSupported
findPreference<Preference>(AppSettings.KEY_APP_VERSION)?.isEnabled = isUpdateSupported && !isLoading
}

}
viewModel.onUpdateAvailable.observeEvent(viewLifecycleOwner, ::onUpdateAvailable)
override fun onDestroyView() {
(view as? ViewGroup)?.removeView(konfettiView)
super.onDestroyView()
}

override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_APP_VERSION -> {
viewModel.checkForUpdates()
versionClickCount++
if (versionClickCount == 8) {
versionClickCount = 0
triggerEasterEgg()
}
true
}

Expand Down Expand Up @@ -82,12 +107,25 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) {
}
}

private fun onUpdateAvailable(version: AppVersion?) {
if (version == null) {
Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show()
} else {
startActivity(Intent(requireContext(), AppUpdateActivity::class.java))
private fun triggerEasterEgg() {
val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.unicorn)
if (drawable == null) {
Snackbar.make(listView, "Failed to load unicorn drawable", Snackbar.LENGTH_SHORT).show()
return
}

val coreImage = DrawableImage(drawable, drawable.intrinsicWidth, drawable.intrinsicHeight)
val drawableShape = Shape.DrawableShape(coreImage, tint = false, applyAlpha = true)

val presets = listOf(
Presets.festive(drawableShape),
Presets.explode(drawableShape),
Presets.parade(drawableShape),
Presets.rain(drawableShape)
)

val randomPreset = presets[Random.nextInt(presets.size)]
konfettiView.start(randomPreset)
}

private fun openLink(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
package io.github.landwarderer.futon.settings.about

import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import io.github.landwarderer.futon.core.github.AppUpdateRepository
import io.github.landwarderer.futon.core.github.AppVersion
import io.github.landwarderer.futon.core.ui.BaseViewModel
import io.github.landwarderer.futon.core.util.ext.MutableEventFlow
import io.github.landwarderer.futon.core.util.ext.call
import javax.inject.Inject

@HiltViewModel
class AboutSettingsViewModel @Inject constructor(
private val appUpdateRepository: AppUpdateRepository,
) : BaseViewModel() {
class AboutSettingsViewModel @Inject constructor() : BaseViewModel() {

val isUpdateSupported = flow {
emit(appUpdateRepository.isUpdateSupported())
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)

val onUpdateAvailable = MutableEventFlow<AppVersion?>()

fun checkForUpdates() {
launchLoadingJob {
val update = appUpdateRepository.fetchUpdate()
onUpdateAvailable.call(update)
}
}
}
Loading
Loading