Skip to content

Commit

Permalink
Merge pull request #21 from Konyaco/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Konyaco authored Feb 24, 2023
2 parents 730889e + 824e23f commit d860eda
Show file tree
Hide file tree
Showing 26 changed files with 862 additions and 638 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ UI is built on [Jetbrains Compose](https://github.com/JetBrains/compose-jb).
- [ ] American Dictionary
- [ ] English Dictionary
- [ ] Synonyms and antonyms
- [ ] Derivative words
- [ ] Some words have comments in COBUILD.
- [ ] Daily word
- [ ] Widget
6 changes: 3 additions & 3 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ dependencies {
implementation(project(":common"))
implementation("org.jsoup:jsoup:${rootProject.extra["jsoup_version"]}")
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.activity:activity-compose:1.6.0")
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import me.konyaco.collinsdictionary.ui.App
import me.konyaco.collinsdictionary.ui.MyTheme
import me.konyaco.collinsdictionary.viewmodel.AppViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class MainActivity : AppCompatActivity() {
private val viewModel by lazy {
(application as MyApplication).viewModel
}
class MainActivity : AppCompatActivity(), KoinComponent {
private val viewModel: AppViewModel by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -19,7 +20,7 @@ class MainActivity : AppCompatActivity() {
MyTheme {
App(viewModel)
BackHandler {
if(viewModel.uiState.value.queryResult != null) {
if (viewModel.uiState.value.queryResult != null) {
viewModel.clearResult()
} else {
finish()
Expand Down
33 changes: 19 additions & 14 deletions android/src/main/java/me/konyaco/collinsdictionary/MyApplication.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
package me.konyaco.collinsdictionary

import android.app.Application
import me.konyaco.collinsdictionary.repository.Repository
import me.konyaco.collinsdictionary.service.CollinsOnlineDictionary
import me.konyaco.collinsdictionary.service.LocalCacheDictionary
import me.konyaco.collinsdictionary.store.FileBasedLocalStorage
import me.konyaco.collinsdictionary.viewmodel.AppViewModel
import me.konyaco.collinsdictionary.store.LocalStorage
import org.koin.core.context.startKoin
import org.koin.dsl.module

class MyApplication : Application() {
lateinit var repository: Repository
lateinit var viewModel: AppViewModel

override fun onCreate() {
super.onCreate()
repository = Repository(
CollinsOnlineDictionary(),
LocalCacheDictionary(FileBasedLocalStorage(filesDir.resolve("cache").also {
if (!it.exists()) it.mkdir()
}))
)
viewModel = AppViewModel(repository)
initKoin()
}

private fun initKoin() {
startKoin {
modules(
commonModule,
module {
single<LocalStorage> {
FileBasedLocalStorage(filesDir.resolve("cache").also {
if (!it.exists()) it.mkdir()
})
}
}
)
}
}
}
3 changes: 2 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
id("com.android.library") version "7.3.1" apply false
kotlin("android") version "1.7.20" apply false
kotlin("multiplatform") version "1.7.20" apply false
id("org.jetbrains.compose") version "1.2.0" apply false
id("org.jetbrains.compose") version "1.3.0" apply false
kotlin("plugin.serialization") version "1.7.20" apply false
}

Expand All @@ -17,6 +17,7 @@ allprojects {
extra["serialization_version"] = "1.4.1"
extra["coroutines_version"] = "1.6.4"
extra["ktor_version"] = "2.1.2"
extra["koin_version"] = "3.2.2"
}

group = "me.konyaco.collinsdictionary"
Expand Down
6 changes: 5 additions & 1 deletion common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ kotlin {
val serializationVersion = extra["serialization_version"]
val jsoupVersion = extra["jsoup_version"]
val coroutinesVersion = extra["coroutines_version"]
val koinVersion = extra["koin_version"]

val commonMain by getting {
dependencies {
Expand All @@ -29,16 +30,19 @@ kotlin {
api("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion")
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
api("io.ktor:ktor-client-cio:$ktorVersion")
api("io.insert-koin:koin-core:$koinVersion")
// implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
}
}
val androidMain by getting {
dependencies {
api("androidx.appcompat:appcompat:1.5.1")
api("androidx.appcompat:appcompat:1.6.1")
api("androidx.core:core-ktx:1.9.0")
api("io.ktor:ktor-client-cio:$ktorVersion")
implementation("org.jsoup:jsoup:$jsoupVersion")
}
}

val jvmMain by getting {
dependencies {
api(compose.desktop.currentOs)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package me.konyaco.collinsdictionary.service

import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.util.Log
import androidx.core.net.toUri
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import java.io.File
import kotlin.coroutines.resume

private const val TAG = "AndroidSoundPlayer.android"

class AndroidSoundPlayer : SoundPlayer {
var context: Context? = null

override fun play(
url: String,
onStart: () -> Unit,
onStop: () -> Unit,
onError: (e: Throwable) -> Unit
) {
job.let {
job = scope.async(Dispatchers.IO) {
val file = try {
getFile(url)
} catch (e: Throwable) {
Log.w(TAG, "Failed to download sound", e)
onError(e)
return@async
}
it?.cancelAndJoin()
try {
onStart()
playMedia(file)
onStop()
} catch (e: Throwable) {
Log.w("Failed to play sound", e)
onError(e)
}
}
}
}

private val scope = CoroutineScope(Dispatchers.Default)
private var job: Job? = null

private suspend fun playMedia(file: File) {
suspendCancellableCoroutine<Unit> { continuation ->
val mediaPlayer = MediaPlayer()
continuation.invokeOnCancellation {
mediaPlayer.pause()
mediaPlayer.release()
}
with(mediaPlayer) {
setOnCompletionListener {
release()
continuation.resume(Unit)
}
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(context!!, file.toUri())
prepare()
start()
}
}
}

private fun directory(): File =
context!!.externalCacheDir!!.resolve("sound").also { it.mkdir() }

// Get filename part of url (xx://xx/xxx.mp3)
private fun fileNameFromUrl(url: String) = url.substringAfterLast("/")

private suspend fun getFile(url: String): File {
val fileName = fileNameFromUrl(url)
val file = directory().resolve(fileName)
if (!file.exists()) {
val bytes = HttpClient().get(url).readBytes()
withContext(Dispatchers.IO) {
file.outputStream().use {
it.write(bytes)
}
}
}
return file
}
}
Original file line number Diff line number Diff line change
@@ -1,107 +1,14 @@
package me.konyaco.collinsdictionary.service

import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import java.io.File
import kotlin.coroutines.resume

private const val TAG = "SoundPlayer.android"

class SoundPlayerImpl : SoundPlayer {
var context: Context? = null

override fun play(
url: String,
onStart: () -> Unit,
onStop: () -> Unit,
onError: (e: Throwable) -> Unit
) {
job.let {
job = scope.async(Dispatchers.IO) {
val file = try {
getFile(url)
} catch (e: Throwable) {
Log.w(TAG, "Failed to download sound", e)
onError(e)
return@async
}
it?.cancelAndJoin()
try {
onStart()
playMedia(file)
onStop()
} catch (e: Throwable) {
Log.w("Failed to play sound", e)
onError(e)
}
}
}
}

private val scope = CoroutineScope(Dispatchers.Default)
private var job: Job? = null

private suspend fun playMedia(file: File) {
suspendCancellableCoroutine<Unit> { continuation ->
val mediaPlayer = MediaPlayer()
continuation.invokeOnCancellation {
mediaPlayer.pause()
mediaPlayer.release()
}
with(mediaPlayer) {
setOnCompletionListener {
release()
continuation.resume(Unit)
}
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
setDataSource(context!!, file.toUri())
prepare()
start()
}
}
}

private fun directory(): File =
context!!.externalCacheDir!!.resolve("sound").also { it.mkdir() }

// Get filename part of url (xx://xx/xxx.mp3)
private fun fileNameFromUrl(url: String) = url.substringAfterLast("/")

private suspend fun getFile(url: String): File {
val fileName = fileNameFromUrl(url)
val file = directory().resolve(fileName)
if (!file.exists()) {
val bytes = HttpClient().get(url).readBytes()
withContext(Dispatchers.IO) {
file.outputStream().use {
it.write(bytes)
}
}
}
return file
}
}

@Composable
actual fun getSoundPlayer(): SoundPlayer {
val context = LocalContext.current.applicationContext
val soundPlayer = remember { SoundPlayerImpl() }
val soundPlayer = remember { AndroidSoundPlayer() }
DisposableEffect(Unit) {
soundPlayer.context = context
onDispose { soundPlayer.context = null }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package me.konyaco.collinsdictionary.ui.util

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent

@OptIn(ExperimentalComposeUiApi::class)
actual fun Modifier.onEnterPress(onPress: () -> Unit) =
this.onKeyEvent {
return@onKeyEvent if (it.key == Key.Enter) {
onPress()
true
} else false
}
Loading

0 comments on commit d860eda

Please sign in to comment.