WIP: Big settings cleaning.

This commit is contained in:
aminecmi 2022-08-24 22:15:42 +02:00
parent fbcb428e96
commit 5531034086
26 changed files with 170 additions and 629 deletions

View File

@ -1,3 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
// TODO: test source adding

View File

@ -1,102 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressKey
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import android.view.KeyEvent
import androidx.test.espresso.matcher.RootMatchers.isDialog
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.utils.Config
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class HomeActivityEspressoTest {
lateinit var context: Context
@Rule @JvmField
val rule = ActivityTestRule(HomeActivity::class.java, true, false)
@Before
fun clearData() {
context = InstrumentationRegistry.getInstrumentation().targetContext
val editor =
context
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
.edit()
editor.clear()
editor.putString("url", BuildConfig.LOGIN_URL)
editor.putString("login", BuildConfig.LOGIN_USERNAME)
editor.putString("password", BuildConfig.LOGIN_PASSWORD)
editor.commit()
Intents.init()
}
@Test
fun menuItems() {
rule.launchActivity(Intent())
onView(
withMenu(
id = R.id.action_search,
titleId = R.string.menu_home_search
)
).perform(click())
onView(withId(R.id.search_bar)).check(matches(isDisplayed()))
onView(withId(R.id.search_src_text)).perform(
typeText("android"),
pressKey(KeyEvent.KEYCODE_SEARCH),
closeSoftKeyboard()
)
onView(withContentDescription(R.string.abc_toolbar_collapse_description)).perform(click())
openActionBarOverflowOrOptionsMenu(context)
onView(withMenu(id = R.id.refresh, titleId = R.string.menu_home_refresh))
.perform(click())
onView(withText(android.R.string.ok))
.inRoot(isDialog()).check(matches(isDisplayed())).perform(click())
openActionBarOverflowOrOptionsMenu(context)
onView(withText(R.string.action_disconnect)).perform(click())
intended(hasComponent(LoginActivity::class.java.name))
}
// TODO: test articles opening and actions for cards and lists
@After
fun releaseIntents() {
Intents.release()
}
}

View File

@ -1,180 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import androidx.test.InstrumentationRegistry
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.utils.Config
import com.mikepenz.aboutlibraries.ui.LibsActivity
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LoginActivityEspressoTest {
@Rule @JvmField
val rule = ActivityTestRule(LoginActivity::class.java, true, false)
private lateinit var context: Context
private lateinit var url: String
private lateinit var username: String
private lateinit var password: String
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
val editor =
context
.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
.edit()
editor.clear()
editor.commit()
url = BuildConfig.LOGIN_URL
username = BuildConfig.LOGIN_USERNAME
password = BuildConfig.LOGIN_PASSWORD
Intents.init()
}
@Test
fun menuItems() {
rule.launchActivity(Intent())
openActionBarOverflowOrOptionsMenu(context)
onView(withText(R.string.action_about)).perform(click())
intended(hasComponent(LibsActivity::class.java.name), times(1))
onView(isRoot()).perform(pressBack())
intended(hasComponent(LoginActivity::class.java.name))
}
@Test
fun wrongLoginUrl() {
rule.launchActivity(Intent())
onView(withId(R.id.loginProgress))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(withId(R.id.urlView)).perform(click()).perform(typeText("WRONGURL"))
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled()))
}
// TODO: Add tests for multiple false urls with dialog
@Test
fun emptyAuthData() {
rule.launchActivity(Intent())
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
onView(withId(R.id.withLogin)).perform(click())
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.loginView)).perform(click()).perform(
typeText(username),
closeSoftKeyboard()
)
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.passwordView)).check(
matches(
isHintOrErrorEnabled()
)
)
}
@Test
fun wrongAuthData() {
rule.launchActivity(Intent())
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
onView(withId(R.id.withLogin)).perform(click())
onView(withId(R.id.loginView)).perform(click()).perform(
typeText(username),
closeSoftKeyboard()
)
onView(withId(R.id.passwordView)).perform(click()).perform(
typeText("WRONGPASS"),
closeSoftKeyboard()
)
onView(withId(R.id.signInButton)).perform(click())
onView(withId(R.id.urlView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.loginView)).check(matches(isHintOrErrorEnabled()))
onView(withId(R.id.passwordView)).check(matches(isHintOrErrorEnabled()))
}
@Test
fun workingAuth() {
rule.launchActivity(Intent())
onView(withId(R.id.urlView)).perform(click()).perform(typeText(url), closeSoftKeyboard())
onView(withId(R.id.withLogin)).perform(click())
onView(withId(R.id.loginView)).perform(click()).perform(
typeText(username),
closeSoftKeyboard()
)
onView(withId(R.id.passwordView)).perform(click()).perform(
typeText(password),
closeSoftKeyboard()
)
onView(withId(R.id.signInButton)).perform(click())
Thread.sleep(2000)
intended(hasComponent(HomeActivity::class.java.name))
}
@After
fun releaseIntents() {
Intents.release()
}
}

View File

@ -1,81 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.test.InstrumentationRegistry.getInstrumentation
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.times
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.AndroidJUnit4
import bou.amine.apps.readerforselfossv2.android.HomeActivity
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.utils.Config
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityEspressoTest {
lateinit var intent: Intent
lateinit var preferencesEditor: SharedPreferences.Editor
private lateinit var url: String
private lateinit var username: String
private lateinit var password: String
@Rule @JvmField
val rule = ActivityTestRule(MainActivity::class.java, true, false)
@Before
fun setUp() {
intent = Intent()
val context = getInstrumentation().targetContext
// create a SharedPreferences editor
preferencesEditor = context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE).edit()
url = BuildConfig.LOGIN_URL
username = BuildConfig.LOGIN_USERNAME
password = BuildConfig.LOGIN_PASSWORD
Intents.init()
}
@Test
fun checkFirstOpenLaunchesIntro() {
preferencesEditor.putString("url", "")
preferencesEditor.putString("password", "")
preferencesEditor.putString("login", "")
preferencesEditor.commit()
rule.launchActivity(intent)
intended(hasComponent(LoginActivity::class.java.name))
intended(hasComponent(HomeActivity::class.java.name), times(0))
}
@Test
fun checkNotFirstOpenLaunchesLogin() {
preferencesEditor.putString("url", url)
preferencesEditor.putString("password", password)
preferencesEditor.putString("login", username)
preferencesEditor.commit()
rule.launchActivity(intent)
intended(hasComponent(MainActivity::class.java.name))
intended(hasComponent(HomeActivity::class.java.name))
}
@After
fun releaseIntents() {
Intents.release()
}
}

View File

@ -1,29 +0,0 @@
package bou.amine.apps.readerforselfossv2.android
import androidx.test.espresso.matcher.ViewMatchers
import android.view.View
import android.widget.EditText
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers
import org.hamcrest.TypeSafeMatcher
fun isHintOrErrorEnabled(): Matcher<View> =
object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description?) {
}
override fun matchesSafely(item: View?): Boolean {
if (item !is EditText) {
return false
}
return item.error.isNotEmpty()
}
}
fun withMenu(id: Int, titleId: Int): Matcher<View> =
Matchers.anyOf(
ViewMatchers.withId(id),
ViewMatchers.withText(titleId)
)

View File

@ -9,10 +9,10 @@ import androidx.constraintlayout.widget.ConstraintLayout
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -31,6 +31,7 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
override val di by closestDI()
private val repository : Repository by instance()
private val apiDetailsService : ApiDetailsService by instance()
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity)
@ -81,9 +82,9 @@ class AddSourceActivity : AppCompatActivity(), DIAware {
override fun onResume() {
super.onResume()
val config = Config()
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
val baseUrl = apiDetailsService.getBaseUrl()
if (baseUrl.isEmpty() || !baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer)

View File

@ -30,13 +30,12 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.utils.*
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
@ -84,10 +83,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private var displayUnreadCount = false
private var displayAllCount = false
private var fullHeightCards: Boolean = false
private var itemsNumber: Int = 200
private var elementsShown: ItemType = ItemType.UNREAD
private var displayAccountHeader: Boolean = false
private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false
private var updateSources: Boolean = true
private var markOnScroll: Boolean = false
@ -114,8 +110,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private lateinit var tagsBadge: Map<Long, Int>
private lateinit var config: Config
override val di by closestDI()
private val repository : Repository by instance()
@ -128,7 +122,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@HomeActivity)
config = Config()
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
@ -308,7 +301,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
reloadLayoutManager()
if (!infiniteScroll) {
if (!settings.getBoolean("infinite_loading", false)) {
binding.recyclerView.setHasFixedSize(true)
} else {
handleInfiniteScroll()
@ -331,15 +324,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
private fun handleSettings() {
// TODO : clean this
internalBrowser = settings.getBoolean("prefer_internal_browser", true)
articleViewer = settings.getBoolean("prefer_article_viewer", true)
shouldBeCardView = settings.getBoolean("card_view_active", false)
displayUnreadCount = settings.getBoolean("display_unread_count", true)
displayAllCount = settings.getBoolean("display_other_count", false)
fullHeightCards = settings.getBoolean("full_height_cards", false)
itemsNumber = settings.getString("prefer_api_items_number", "200").toInt()
displayAccountHeader = settings.getBoolean("account_header_displaying", false)
infiniteScroll = settings.getBoolean("infinite_loading", false)
updateSources = settings.getBoolean("update_sources", true)
markOnScroll = settings.getBoolean("mark_on_scroll", false)
hiddenTags = if (settings.getString("hidden_tags", "").isNotEmpty()) {
@ -409,7 +401,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
binding.drawerContainer.addDrawerListener(drawerListener)
displayAccountHeader =
val displayAccountHeader =
settings.getBoolean("account_header_displaying", false)
binding.mainDrawer.addStickyFooterItem(
@ -418,7 +410,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
iconRes = R.drawable.ic_bug_report_black_24dp
isIconTinted = true
onDrawerItemClickListener = { _, _, _ ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(Config.trackerUrl))
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(ApiDetailsService.trackerUrl))
startActivity(browserIntent)
false
}
@ -451,6 +443,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
// TODO: refactor this.
// TODO: use updateSources
private fun handleDrawerItems() {
tagsBadge = emptyMap()
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
@ -875,11 +868,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
this,
items,
customTabActivityHelper,
internalBrowser,
articleViewer,
fullHeightCards,
internalBrowser, // TODO remove and use from apidetailsservice
articleViewer, // TODO remove and use from apidetailsservice
fullHeightCards, // TODO remove and use from apidetailsservice
appColors,
config
) {
updateItems(it)
}
@ -889,10 +881,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
this,
items,
customTabActivityHelper,
internalBrowser,
articleViewer,
internalBrowser, // TODO remove and use from apidetailsservice
articleViewer, // TODO remove and use from apidetailsservice
appColors,
config
) {
updateItems(it)
}
@ -1047,7 +1038,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
return true
}
R.id.action_disconnect -> {
return Config.logoutAndRedirect(this, this@HomeActivity)
val settings = Settings()
settings.clear()
val intent = Intent(this, LoginActivity::class.java)
this.startActivity(intent)
this@HomeActivity.finish()
return true
}
else -> return super.onOptionsItemSelected(item)
}

View File

@ -14,12 +14,11 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.dao.DriverFactory
import bou.amine.apps.readerforselfossv2.dao.ReaderForSelfossDB
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
@ -49,13 +48,11 @@ class MyApp : MultiDexApplication(), DIAware {
private val viewModel: AppViewModel by instance()
private val connectivityStatus: ConnectivityStatus by instance()
private val driverFactory: DriverFactory by instance()
private lateinit var config: Config
private lateinit var settings : Settings
override fun onCreate() {
super.onCreate()
Napier.base(DebugAntilog())
config = Config()
settings = Settings()
initDrawerImageLoader()
@ -93,11 +90,11 @@ class MyApp : MultiDexApplication(), DIAware {
val name = getString(R.string.notification_channel_sync)
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel(Config.syncChannelId, name, importance)
val mChannel = NotificationChannel(ApiDetailsService.syncChannelId, name, importance)
val newItemsChannelname = getString(R.string.new_items_channel_sync)
val newItemsChannelimportance = NotificationManager.IMPORTANCE_DEFAULT
val newItemsChannelmChannel = NotificationChannel(Config.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
val newItemsChannelmChannel = NotificationChannel(ApiDetailsService.newItemsChannelId, newItemsChannelname, newItemsChannelimportance)
notificationManager.createNotificationChannel(mChannel)
notificationManager.createNotificationChannel(newItemsChannelmChannel)
@ -108,7 +105,7 @@ class MyApp : MultiDexApplication(), DIAware {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
Glide.with(imageView.context)
.loadMaybeBasicAuth(config, uri.toString())
.load(uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView)
}

View File

@ -38,7 +38,6 @@ class ItemCardAdapter(
private val articleViewer: Boolean,
private val fullHeightCards: Boolean,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext
@ -78,7 +77,7 @@ class ItemCardAdapter(
binding.itemImage.setImageDrawable(null)
} else {
binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
if (itm.getIcon(repository.baseUrl).isEmpty()) {
@ -91,7 +90,7 @@ class ItemCardAdapter(
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage)
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.sourceImage)
}
}
}

View File

@ -30,7 +30,6 @@ class ItemListAdapter(
private val internalBrowser: Boolean,
private val articleViewer: Boolean,
override val appColors: AppColors,
override val config: Config,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL
@ -69,10 +68,10 @@ class ItemListAdapter(
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
} else {
c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage)
c.bitmapCenterCrop(itm.getThumbnail(repository.baseUrl), binding.itemImage)
}
}
}

View File

@ -6,7 +6,6 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.ItemType
@ -21,7 +20,6 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
abstract val repository: Repository
abstract val app: Activity
abstract val appColors: AppColors
abstract val config: Config
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
fun updateAllItems(items: ArrayList<SelfossModel.Item>) {

View File

@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.toTextDrawableString
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.model.SelfossModel
@ -33,7 +32,6 @@ class SourcesListAdapter(
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config
private lateinit var binding: SourceListItemBinding
override val di: DI by closestDI(app)
@ -46,7 +44,6 @@ class SourcesListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position]
config = Config()
if (itm.getIcon(repository.baseUrl).isEmpty()) {
val color = generator.getColor(itm.title.getHtmlDecoded())
@ -58,7 +55,7 @@ class SourcesListAdapter(
.build(itm.title.getHtmlDecoded().toTextDrawableString(), color)
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
c.circularBitmapDrawable(itm.getIcon(repository.baseUrl), binding.itemImage)
}
binding.sourceTitle.text = itm.title.getHtmlDecoded()

View File

@ -14,11 +14,10 @@ import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.MyApp
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.preloadImages
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -27,7 +26,6 @@ import org.kodein.di.DIAware
import org.kodein.di.instance
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware {
@ -44,12 +42,12 @@ override fun doWork(): Result {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
NotificationCompat.Builder(applicationContext, ApiDetailsService.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setChannelId(ApiDetailsService.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
@ -89,7 +87,7 @@ override fun doWork(): Result {
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, pflags)
val newItemsNotification =
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
NotificationCompat.Builder(applicationContext, ApiDetailsService.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
@ -98,7 +96,7 @@ override fun doWork(): Result {
)
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setChannelId(ApiDetailsService.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)

View File

@ -29,7 +29,6 @@ import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
@ -70,7 +69,6 @@ class ArticleFragment : Fragment(), DIAware {
private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
@ -93,7 +91,6 @@ class ArticleFragment : Fragment(), DIAware {
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(requireActivity())
config = Config()
super.onCreate(savedInstanceState)
@ -213,7 +210,7 @@ class ArticleFragment : Fragment(), DIAware {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(config, contentImage)
.load(contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
} else {
@ -307,8 +304,7 @@ class ArticleFragment : Fragment(), DIAware {
Glide
.with(requireContext())
.asBitmap()
.loadMaybeBasicAuth(
config,
.load(
response.body()!!.lead_image_url.orEmpty()
)
.apply(RequestOptions.fitCenterTransform())

View File

@ -15,7 +15,7 @@ import androidx.preference.PreferenceFragmentCompat
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBinding
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import java.lang.NumberFormatException
@ -196,17 +196,17 @@ class SettingsActivity : AppCompatActivity(),
setPreferencesFromResource(R.xml.pref_links, rootKey)
preferenceManager.findPreference<Preference>("trackerLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.trackerUrl))
openUrl(Uri.parse(ApiDetailsService.trackerUrl))
true
}
preferenceManager.findPreference<Preference>("sourceLink")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.sourceUrl))
openUrl(Uri.parse(ApiDetailsService.sourceUrl))
false
}
preferenceManager.findPreference<Preference>("translation")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openUrl(Uri.parse(Config.translationUrl))
openUrl(Uri.parse(ApiDetailsService.translationUrl))
false
}
}

View File

@ -1,62 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.LoginActivity
import com.russhwolf.settings.Settings
class Config {
val settings = Settings()
val baseUrl: String
get() = settings.getString("url", "")
val userLogin: String
get() = settings.getString("login", "")
val userPassword: String
get() = settings.getString("password", "")
val httpUserLogin: String
get() = settings.getString("httpUserName", "")
val httpUserPassword: String
get() = settings.getString("httpPassword", "")
companion object {
const val settingsName = "paramsselfoss"
const val feedbackEmail = "aminecmi@pm.me.com"
const val translationUrl = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val trackerUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val syncChannelId = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
var apiVersion = 0
/* Execute logout and clear all settings to default */
fun logoutAndRedirect(
c: Context,
callingActivity: Activity,
baseUrlFail: Boolean = false
): Boolean {
val settings = Settings()
settings.clear()
val intent = Intent(c, LoginActivity::class.java)
if (baseUrlFail) {
intent.putExtra("baseUrlFail", baseUrlFail)
}
c.startActivity(intent)
callingActivity.finish()
return true
}
}
}

View File

@ -2,33 +2,26 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.util.Base64
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import android.widget.ImageView
import bou.amine.apps.readerforselfossv2.android.utils.Config
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.BitmapImageViewTarget
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
fun Context.bitmapCenterCrop(config: Config, url: String, iv: ImageView) =
fun Context.bitmapCenterCrop(url: String, iv: ImageView) =
Glide.with(this)
.asBitmap()
.loadMaybeBasicAuth(config, url)
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(iv)
fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
fun Context.circularBitmapDrawable(url: String, iv: ImageView) =
Glide.with(this)
.asBitmap()
.loadMaybeBasicAuth(config, url)
.load(url)
.apply(RequestOptions.centerCropTransform())
.into(object : BitmapImageViewTarget(iv) {
override fun setResource(resource: Bitmap?) {
@ -41,26 +34,6 @@ fun Context.circularBitmapDrawable(config: Config, url: String, iv: ImageView) =
}
})
fun RequestBuilder<Bitmap>.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Bitmap> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun RequestManager.loadMaybeBasicAuth(config: Config, url: String): RequestBuilder<Drawable> {
val builder: LazyHeaders.Builder = LazyHeaders.Builder()
if (config.httpUserLogin.isNotEmpty() || config.httpUserPassword.isNotEmpty()) {
val basicAuth = "Basic " + Base64.encodeToString("${config.httpUserLogin}:${config.httpUserPassword}".toByteArray(), Base64.NO_WRAP)
builder.addHeader("Authorization", basicAuth)
}
val glideUrl = GlideUrl(url, builder.build())
return this.load(glideUrl)
}
fun getBitmapInputStream(bitmap:Bitmap,compressFormat: Bitmap.CompressFormat): InputStream {
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(compressFormat, 80, byteArrayOutputStream)

View File

@ -1,18 +1,21 @@
package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
actual class DateUtils actual constructor(private val apiMajorVersion: Int) {
actual class DateUtils actual constructor(apiDetailsService: ApiDetailsService) {
val ads: ApiDetailsService = apiDetailsService // TODO: why is this needed now ?
actual fun parseDate(dateString: String): Long {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiMajorVersion >= 4) {
return if (ads.getApiVersion() >= 4) {
OffsetDateTime.parse(dateString).toInstant().toEpochMilli()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(

View File

@ -2,13 +2,12 @@ package bou.amine.apps.readerforselfossv2.DI
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.ApiDetailsServiceImpl
import org.kodein.di.DI
import org.kodein.di.bind
import org.kodein.di.instance
import org.kodein.di.singleton
val networkModule by DI.Module {
bind<ApiDetailsService>() with singleton { ApiDetailsServiceImpl() }
bind<ApiDetailsService>() with singleton { ApiDetailsService() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) }
}

View File

@ -32,7 +32,6 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
var itemsCaching = settings.getBoolean("items_caching", false)
var offlineOverride = false
var apiMajorVersion = 0
var badgeUnread = 0
set(value) {field = if (value < 0) { 0 } else { value } }
var badgeAll = 0
@ -44,6 +43,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch {
updateApiVersion()
dateUtils = DateUtils(apiDetails)
reloadBadges()
}
}
@ -54,7 +54,6 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
if (isNetworkAvailable()) {
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset = 0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
@ -84,7 +83,6 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
val offset = items.size
fetchedItems = api.getItems(
displayedItems.type,
settings.getString("prefer_api_items_number", "200").toInt(),
offset,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
@ -104,12 +102,12 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
return if (isNetworkAvailable()) {
api.getItems(
itemType.type,
200,
0,
tagFilter?.tag,
sourceFilter?.id?.toLong(),
searchFilter,
null
null,
200
)
} else {
emptyList()
@ -309,7 +307,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
spout,
tags,
filter,
apiMajorVersion
apiDetails.getApiVersion()
)?.isSuccess == true
}
@ -366,13 +364,13 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
private suspend fun updateApiVersion() {
apiMajorVersion = settings.getInt("apiVersionMajor", 0)
val apiMajorVersion = apiDetails.getApiVersion()
if (isNetworkAvailable()) {
val fetchedVersion = api.version()
if (fetchedVersion != null) {
apiMajorVersion = fetchedVersion.getApiMajorVersion()
settings.putInt("apiVersionMajor", apiMajorVersion)
if (fetchedVersion != null && fetchedVersion.getApiMajorVersion() != apiMajorVersion) {
settings.putInt("apiVersionMajor", fetchedVersion.getApiMajorVersion())
apiDetails.refreshApiVersion()
}
}
dateUtils = DateUtils(apiMajorVersion)

View File

@ -4,6 +4,7 @@ import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
@ -35,6 +36,9 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
}
level = LogLevel.ALL
}
install(HttpTimeout) {
requestTimeoutMillis = apiDetailsService.getApiTimeout()
}
/* TODO: Auth as basic
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
@ -69,12 +73,12 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
suspend fun getItems(
type: String,
items: Int,
offset: Int,
tag: String?,
source: Long?,
search: String?,
updatedSince: String?
updatedSince: String?,
items: Int? = null
): List<SelfossModel.Item>? =
client.get(url("/items")) {
parameter("username", apiDetailsService.getUserName())
@ -84,7 +88,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
parameter("source", source)
parameter("search", search)
parameter("updatedsince", updatedSince)
parameter("items", items)
parameter("items", items ?: apiDetailsService.getItemsNumber())
parameter("offset", offset)
}.body()

View File

@ -1,10 +1,97 @@
package bou.amine.apps.readerforselfossv2.service
interface ApiDetailsService {
fun logApiCalls(message: String)
fun getApiVersion(): Int
fun getBaseUrl(): String
fun getUserName(): String
fun getPassword(): String
fun refresh()
import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import io.ktor.client.plugins.*
class ApiDetailsService {
val settings: Settings = Settings()
private var _apiVersion: Int = -1
private var _baseUrl: String = ""
private var _userName: String = ""
private var _password: String = ""
private var _itemsNumber: Int? = null
private var _apiTimeout: Long? = null
fun logApiCalls(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
fun getApiVersion(): Int {
if (_apiVersion == -1) {
refreshApiVersion()
return _apiVersion
}
return _apiVersion
}
fun refreshApiVersion() {
_apiVersion = settings.getInt("apiVersionMajor", -1)
}
fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) {
_baseUrl = settings.getString("url", "")
}
return _baseUrl
}
fun getUserName(): String {
if (_userName.isEmpty()) {
_userName = settings.getString("login", "")
}
return _userName
}
fun getPassword(): String {
if (_password.isEmpty()) {
_password = settings.getString("password", "")
}
return _password
}
fun getItemsNumber(): Int {
if (_itemsNumber == null) {
refreshItemsNumber()
}
return _itemsNumber!!
}
fun refreshItemsNumber() {
_itemsNumber = settings.getString("prefer_api_items_number", "200").toInt()
}
fun getApiTimeout(): Long {
if (_apiTimeout == null) {
refreshApiTimeout()
}
return _apiTimeout!!
}
fun refreshApiTimeout() {
val settingsTimeout = settings.getLong("api_timeout", HttpTimeout.INFINITE_TIMEOUT_MS)
_apiTimeout = if (settingsTimeout > 0) settingsTimeout else HttpTimeout.INFINITE_TIMEOUT_MS
}
fun refresh() {
_password = settings.getString("password", "")
_userName = settings.getString("login", "")
_baseUrl = settings.getString("url", "")
refreshApiVersion()
refreshItemsNumber()
refreshApiTimeout()
}
companion object {
const val translationUrl = "https://crwd.in/readerforselfoss"
const val sourceUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform"
const val trackerUrl = "https://gitea.amine-louveau.fr/Louvorg/ReaderForSelfoss-multiplatform/issues"
const val syncChannelId = "sync-channel-id"
const val newItemsChannelId = "new-items-channel-id"
}
}

View File

@ -1,52 +0,0 @@
package bou.amine.apps.readerforselfossv2.service
import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
class ApiDetailsServiceImpl : ApiDetailsService {
val settings: Settings = Settings()
private var _apiVersion: Int = -1
private var _baseUrl: String = ""
private var _userName: String = ""
private var _password: String = ""
override fun logApiCalls(message: String) {
Napier.d(message, tag = "LogApiCalls")
}
override fun getApiVersion(): Int {
if (_apiVersion == -1) {
_apiVersion = settings.getInt("apiVersionMajor", -1)
return _apiVersion
}
return _apiVersion
}
override fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) {
_baseUrl = settings.getString("url", "")
}
return _baseUrl
}
override fun getUserName(): String {
if (_userName.isEmpty()) {
_userName = settings.getString("login", "")
}
return _userName
}
override fun getPassword(): String {
if (_password.isEmpty()) {
_password = settings.getString("password", "")
}
return _password
}
override fun refresh() {
_password = settings.getString("password", "")
_userName = settings.getString("login", "")
_baseUrl = settings.getString("url", "")
_apiVersion = settings.getInt("apiVersionMajor", -1)
}
}

View File

@ -1,12 +1,13 @@
package bou.amine.apps.readerforselfossv2.utils
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
fun SelfossModel.Item.parseDate(dateUtils: DateUtils): Long =
dateUtils.parseDate(this.datetime)
expect class DateUtils(apiMajorVersion: Int) {
expect class DateUtils(apiDetailsService: ApiDetailsService) {
fun parseDate(dateString: String): Long
fun parseRelativeDate(dateString: String): String

View File

@ -1,6 +1,8 @@
package bou.amine.apps.readerforselfossv2.utils
actual class DateUtils actual constructor(apiMajorVersion: Int) {
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
actual class DateUtils actual constructor(apiDetailsService: ApiDetailsService) {
actual fun parseDate(dateString: String): Long {
TODO("Not yet implemented")
}

View File

@ -1,6 +1,8 @@
package bou.amine.apps.readerforselfossv2.utils
actual class DateUtils actual constructor(apiMajorVersion: Int) {
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
actual class DateUtils actual constructor(apiDetailsService: ApiDetailsService) {
actual fun parseDate(dateString: String): Long {
TODO("Not yet implemented")
}