Skip to content

Commit

Permalink
[Code] Some improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
Konyaco committed Oct 22, 2021
1 parent f352641 commit 9d7222e
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 172 deletions.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id("com.android.application") version "7.1.0-alpha13" apply false
id("com.android.library") version "7.1.0-alpha13" apply false
id("com.android.application") version "7.2.0-alpha02" apply false
id("com.android.library") version "7.2.0-alpha02" apply false
kotlin("android") version "1.5.31" apply false
kotlin("multiplatform") version "1.5.31" apply false
id("org.jetbrains.compose") version "1.0.0-alpha4-build398" apply false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import android.media.AudioAttributes
import android.media.MediaPlayer
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
Expand All @@ -17,7 +17,7 @@ import java.net.URL
private const val TAG = "SoundPlayer.android"

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

private val scope = CoroutineScope(Dispatchers.Default)

Expand All @@ -29,37 +29,31 @@ class SoundPlayerImpl : SoundPlayer {
) {
scope.launch(Dispatchers.IO) {
try {
Log.d(TAG, "Try to play: $url")
val fileName =
url.substringAfterLast("/") // Get filename part of url (xx://xx/xxx.mp3)
val file = context.externalCacheDir!!.resolve("sound")
val file = context!!.externalCacheDir!!.resolve("sound")
.also { it.mkdir() }
.resolve(fileName)
if (!file.exists()) {
Log.d(TAG, "Caching file $fileName")
val bytes = URL(url).readBytes()
file.outputStream().use {
it.write(bytes)
}
}
MediaPlayer().apply {
setDataSource(context, file.toUri())
setDataSource(context!!, file.toUri())
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
prepare()
Log.d(TAG, "Prepared")
onStart()
start()
Log.d(TAG, "Started")
setOnCompletionListener {
Log.d(TAG, "Stopped")
onStop()
release()
Log.d(TAG, "Released")
}
}
} catch (e: Throwable) {
Expand All @@ -70,13 +64,13 @@ class SoundPlayerImpl : SoundPlayer {
}
}

actual val soundPlayer: SoundPlayer
@Composable
get() {
val context = LocalContext.current.applicationContext
val soundPlayer = remember { SoundPlayerImpl() }
LaunchedEffect(null) {
soundPlayer.context = context
}
return soundPlayer
}
@Composable
actual fun getSoundPlayer(): SoundPlayer {
val context = LocalContext.current.applicationContext
val soundPlayer = remember { SoundPlayerImpl() }
DisposableEffect(Unit) {
soundPlayer.context = context
onDispose { soundPlayer.context = null }
}
return soundPlayer
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package me.konyaco.collinsdictionary.service

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf

interface SoundPlayer {
fun play(url: String, onStart: () -> Unit, onStop: () -> Unit, onError: (e: Throwable) -> Unit)
}

expect val soundPlayer: SoundPlayer
@Composable
get
@Composable
expect fun getSoundPlayer(): SoundPlayer

val LocalSoundPlayer = compositionLocalOf<SoundPlayer> { error("No default impl") }

@Composable
fun ProvideSoundPlayer(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalSoundPlayer provides getSoundPlayer(), content = content)
}
152 changes: 91 additions & 61 deletions common/src/commonMain/kotlin/me/konyaco/collinsdictionary/ui/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import me.konyaco.collinsdictionary.service.ProvideSoundPlayer
import me.konyaco.collinsdictionary.service.Word
import me.konyaco.collinsdictionary.ui.component.CobuildDictionarySection
import me.konyaco.collinsdictionary.ui.component.SearchBox

Expand All @@ -31,90 +34,104 @@ fun ProvideLocalScreenSize(content: @Composable () -> Unit) {
var screenSize by remember { mutableStateOf(ScreenSize.PHONE) }
BoxWithConstraints {
val width = with(LocalDensity.current) { constraints.maxWidth.toDp() }
screenSize = when {
width < 600.dp -> ScreenSize.PHONE
width >= 600.dp && width < 1240.dp -> ScreenSize.TABLET
width >= 1240.dp && width < 1440.dp -> ScreenSize.LAPTOP
width >= 1440.dp -> ScreenSize.DESKTOP
else -> error("error")
LaunchedEffect(width) {
screenSize = when {
width < 600.dp -> ScreenSize.PHONE
width >= 600.dp && width < 1240.dp -> ScreenSize.TABLET
width >= 1240.dp && width < 1440.dp -> ScreenSize.LAPTOP
width >= 1440.dp -> ScreenSize.DESKTOP
else -> error("error")
}
}
CompositionLocalProvider(LocalScreenSize provides screenSize, content = content)
}
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun App(viewModel: AppViewModel) {
ProvideLocalScreenSize {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
val queryState by viewModel.queryState.collectAsState()

val padding = when (LocalScreenSize.current) {
ScreenSize.PHONE -> 16.dp
ScreenSize.TABLET -> 24.dp
ScreenSize.LAPTOP -> 48.dp
ScreenSize.DESKTOP -> 48.dp
}
App(viewModel.queryState.collectAsState().value) { viewModel.search(it) }
}

Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
AnimatedVisibility(queryState is AppViewModel.State.None) {
Box(Modifier.fillMaxHeight(0.5f), contentAlignment = Alignment.BottomStart) {
Column(Modifier.padding(vertical = 16.dp, horizontal = padding)) {
Title()
Spacer(Modifier.height(16.dp))
CollinsDivider()
}
}
sealed class State {
object None : State()
object Searching : State()
data class Succeed(val data: Word) : State()
object WordNotFound : State()
data class Failed(val message: String) : State()
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun App(uiState: State, onSearch: (text: String) -> Unit) {
ProvideSoundPlayer {
ProvideLocalScreenSize {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
val padding = when (LocalScreenSize.current) {
ScreenSize.PHONE -> 16.dp
ScreenSize.TABLET -> 24.dp
ScreenSize.LAPTOP -> 48.dp
ScreenSize.DESKTOP -> 48.dp
}

Spacer(Modifier.height(32.dp))
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
Banner(uiState is State.None, padding)

var input by remember { mutableStateOf("") }
SearchBox(
modifier = Modifier.padding(horizontal = padding).fillMaxWidth(),
value = input,
onValueChange = { input = it },
onSearchClick = { viewModel.search(input) }
)
Spacer(Modifier.height(32.dp))

AnimatedContent(targetState = queryState) { state ->
when (state) {
AppViewModel.State.None -> {
}
is AppViewModel.State.Succeed -> {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Spacer(Modifier.height(4.dp))
state.data.cobuildDictionary.sections.forEach { section ->
CobuildDictionarySection(
section = section,
modifier = Modifier.fillMaxWidth().padding(horizontal = padding)
)
var input by remember { mutableStateOf("") }
SearchBox(
modifier = Modifier.padding(horizontal = padding).fillMaxWidth(),
value = input,
onValueChange = { input = it },
onSearchClick = { onSearch(input) }
)

AnimatedContent(
modifier = Modifier.weight(1f).fillMaxWidth(),
targetState = uiState
) { state ->
when (state) {
State.None -> {
}
is State.Succeed -> {
Content(state.data, padding)
}
is State.Failed -> {
ErrorPage(state.message, Modifier.fillMaxWidth())
}
State.Searching -> {
Box(Modifier.fillMaxWidth().padding(horizontal = padding)) {
LinearProgressIndicator(Modifier.fillMaxWidth())
}
}
}
is AppViewModel.State.Failed -> {
ErrorPage(state.message, Modifier.fillMaxWidth())
}
AppViewModel.State.Searching -> {
Box(Modifier.fillMaxWidth().padding(horizontal = padding)) {
LinearProgressIndicator(Modifier.fillMaxWidth())
State.WordNotFound -> {
WordNotFound(Modifier.fillMaxSize().padding(vertical = 32.dp))
}
}
AppViewModel.State.WordNotFound -> {
Spacer(Modifier.height(16.dp))
WordNotFound(Modifier.fillMaxSize())
}
}
}
}
}
}
}

@Composable
private fun Banner(display: Boolean, contentPadding: Dp) {
AnimatedVisibility(display) {
Box(Modifier.fillMaxHeight(0.5f), contentAlignment = Alignment.BottomStart) {
Column(Modifier.padding(vertical = 16.dp, horizontal = contentPadding)) {
Title()
Spacer(Modifier.height(16.dp))
CollinsDivider()
}
}
}
}

@Composable
private fun Title(modifier: Modifier = Modifier) {
Text(
Expand All @@ -133,6 +150,19 @@ private fun CollinsDivider() {
Divider(Modifier.width(88.dp), color = MaterialTheme.colors.primary, thickness = 4.dp)
}

@Composable
private fun Content(word: Word, contentPadding: Dp) {
ColumnWithScrollBar(Modifier.fillMaxWidth()) {
Spacer(Modifier.height(4.dp))
word.cobuildDictionary.sections.forEach { section ->
CobuildDictionarySection(
section = section,
modifier = Modifier.fillMaxWidth().padding(horizontal = contentPadding)
)
}
}
}

@Composable
private fun ErrorPage(message: String, modifier: Modifier = Modifier) {
Box(modifier.padding(24.dp)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import me.konyaco.collinsdictionary.service.CollinsOnlineDictionary
import me.konyaco.collinsdictionary.service.SearchResult
import me.konyaco.collinsdictionary.service.Word

class AppViewModel {
private val collinsDictionary = CollinsOnlineDictionary()
private val scope = CoroutineScope(Dispatchers.Default)

var queryState: MutableStateFlow<State> = MutableStateFlow(State.None)

sealed class State {
object None : State()
object Searching : State()
data class Succeed(val data: Word) : State()
object WordNotFound : State()
data class Failed(val message: String) : State()
}

fun search(word: String) {
scope.launch {
queryState.emit(State.Searching)
Expand Down
Loading

0 comments on commit 9d7222e

Please sign in to comment.