diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/data/ApiClient.kt b/app/src/main/java/com/mkfunproj/mobilemkch/data/ApiClient.kt index fc62c00..98e1d80 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/data/ApiClient.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/data/ApiClient.kt @@ -16,6 +16,9 @@ import retrofit2.Retrofit import retrofit2.converter.scalars.ScalarsConverterFactory import java.io.File import java.util.concurrent.TimeUnit +import okhttp3.FormBody +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class ApiClient(context: Context) { private val baseUrl = "https://mkch.pooziqo.xyz" @@ -24,6 +27,7 @@ class ApiClient(context: Context) { private val json = Json { ignoreUnknownKeys = true } private val prefs = context.getSharedPreferences("api_prefs", Context.MODE_PRIVATE) + private val cache = Cache(context) private val userAgentInterceptor = Interceptor { chain -> val request = chain.request().newBuilder() @@ -61,63 +65,78 @@ class ApiClient(context: Context) { } } - fun getBoards(): List = parseBoards(get("${apiUrl}boards/")) - - fun getThreads(boardCode: String): List = parseThreads(get("${apiUrl}board/$boardCode")) - - fun getThreadDetail(boardCode: String, threadId: Int): ThreadDetail = - parseThreadDetail(get("${apiUrl}board/$boardCode/thread/$threadId")) - - fun getComments(boardCode: String, threadId: Int): List = - parseComments(get("${apiUrl}board/$boardCode/thread/$threadId/comments")) - - fun getFullThread(boardCode: String, threadId: Int): Pair> { - val detail = getThreadDetail(boardCode, threadId) - val comments = getComments(boardCode, threadId) - return detail to comments + suspend fun getBoards(): List = withContext(Dispatchers.IO) { + parseBoards(get("${apiUrl}boards/")) } - fun authenticate(authKey: String) { - if (authKey.isEmpty()) return + suspend fun getThreads(boardCode: String): List = withContext(Dispatchers.IO) { + parseThreads(get("${apiUrl}board/$boardCode")) + } + + suspend fun getThreadDetail(boardCode: String, threadId: Int): ThreadDetail = withContext(Dispatchers.IO) { + parseThreadDetail(get("${apiUrl}board/$boardCode/thread/$threadId")) + } + + suspend fun getComments(boardCode: String, threadId: Int): List = withContext(Dispatchers.IO) { + parseComments(get("${apiUrl}board/$boardCode/thread/$threadId/comments")) + } + + suspend fun getFullThread(boardCode: String, threadId: Int): Pair> = withContext(Dispatchers.IO) { + val detail = getThreadDetail(boardCode, threadId) + val comments = getComments(boardCode, threadId) + detail to comments + } + + suspend fun authenticate(authKey: String) = withContext(Dispatchers.IO) { + if (authKey.isEmpty()) return@withContext val url = "$baseUrl/key/auth/" val formHtml = get(url) val csrf = extractCsrf(formHtml) - val body = "csrfmiddlewaretoken=${'$'}csrf&key=${'$'}authKey" - .toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val body = FormBody.Builder() + .add("csrfmiddlewaretoken", csrf) + .add("key", authKey) + .build() val req = Request.Builder().url(url) .post(body) .header("Referer", url) .build() httpClient.newCall(req).execute().use { resp -> - if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("auth failed ${'$'}{resp.code}") + if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("Auth failed: ${'$'}{resp.code}") } } - fun loginWithPasscode(passcode: String) { - if (passcode.isEmpty()) return + suspend fun loginWithPasscode(passcode: String) = withContext(Dispatchers.IO) { + if (passcode.isEmpty()) return@withContext val url = "$baseUrl/passcode/enter/" val formHtml = get(url) val csrf = extractCsrf(formHtml) - val body = "csrfmiddlewaretoken=${'$'}csrf&passcode=${'$'}passcode" - .toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val body = FormBody.Builder() + .add("csrfmiddlewaretoken", csrf) + .add("passcode", passcode) + .build() val req = Request.Builder().url(url) .post(body) .header("Referer", url) .build() httpClient.newCall(req).execute().use { resp -> - if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("passcode failed ${'$'}{resp.code}") + if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("Passcode failed: ${'$'}{resp.code}") } } - fun createThread(boardCode: String, title: String, text: String, files: List = emptyList(), passcode: String = "", authKey: String = "") { + suspend fun createThread(boardCode: String, title: String, text: String, files: List = emptyList(), passcode: String = "", authKey: String = "") = withContext(Dispatchers.IO) { if (authKey.isNotEmpty()) authenticate(authKey) if (passcode.isNotEmpty()) loginWithPasscode(passcode) val formUrl = "$baseUrl/boards/board/$boardCode/new" val csrf = fetchCsrf(formUrl) val req = if (files.isEmpty()) { - val body = "csrfmiddlewaretoken=$csrf&title=$title&text=$text" - .toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val body = FormBody.Builder() + .add("csrfmiddlewaretoken", csrf) + .add("title", title) + .add("text", text) + .build() Request.Builder().url(formUrl).post(body) + .header("Referer", formUrl) + .build() } else { val builder = MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("csrfmiddlewaretoken", csrf) @@ -128,23 +147,28 @@ class ApiClient(context: Context) { builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb) } Request.Builder().url(formUrl).post(builder.build()) + .header("Referer", formUrl) + .build() } - .header("Referer", formUrl) - .build() httpClient.newCall(req).execute().use { resp -> if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("createThread failed ${resp.code}") + cache.delete("threads_${boardCode}") } } - fun addComment(boardCode: String, threadId: Int, text: String, files: List = emptyList(), passcode: String = "", authKey: String = "") { + suspend fun addComment(boardCode: String, threadId: Int, text: String, files: List = emptyList(), passcode: String = "", authKey: String = "") = withContext(Dispatchers.IO) { if (authKey.isNotEmpty()) authenticate(authKey) if (passcode.isNotEmpty()) loginWithPasscode(passcode) val formUrl = "$baseUrl/boards/board/$boardCode/thread/$threadId/comment" val csrf = fetchCsrf(formUrl) val req = if (files.isEmpty()) { - val body = "csrfmiddlewaretoken=$csrf&text=$text" - .toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val body = FormBody.Builder() + .add("csrfmiddlewaretoken", csrf) + .add("text", text) + .build() Request.Builder().url(formUrl).post(body) + .header("Referer", formUrl) + .build() } else { val builder = MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("csrfmiddlewaretoken", csrf) @@ -154,11 +178,12 @@ class ApiClient(context: Context) { builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb) } Request.Builder().url(formUrl).post(builder.build()) + .header("Referer", formUrl) + .build() } - .header("Referer", formUrl) - .build() httpClient.newCall(req).execute().use { resp -> if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("addComment failed ${resp.code}") + cache.delete("comments_${threadId}") } } @@ -173,11 +198,11 @@ class ApiClient(context: Context) { } } - fun checkNewThreads(boardCode: String): List { + suspend fun checkNewThreads(boardCode: String): List = withContext(Dispatchers.IO) { val current = getThreads(boardCode) - val key = "savedThreads_${'$'}boardCode" + val key = "savedThreads_${boardCode}" val savedRaw = prefs.getString(key, null) - return if (savedRaw != null) { + return@withContext if (savedRaw != null) { val saved = runCatching { json.decodeFromString>(savedRaw) }.getOrElse { emptyList() } val savedIds = saved.map { it.id }.toSet() val newOnes = current.filter { it.id !in savedIds } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/data/Cache.kt b/app/src/main/java/com/mkfunproj/mobilemkch/data/Cache.kt index 7119a4a..dbf547b 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/data/Cache.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/data/Cache.kt @@ -39,6 +39,10 @@ class Cache(private val context: Context) { File(dir, safeName(key)).delete() } + suspend fun clearAll() = withContext(Dispatchers.IO) { + dir.listFiles()?.forEach { runCatching { it.delete() } } + } + private fun safeName(key: String) = key.hashCode().toString(16) + ".json" } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/data/Cookies.kt b/app/src/main/java/com/mkfunproj/mobilemkch/data/Cookies.kt index bbeeef7..6073e3b 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/data/Cookies.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/data/Cookies.kt @@ -11,16 +11,16 @@ class PersistentCookieJar(context: Context) : CookieJar { override fun saveFromResponse(url: HttpUrl, cookies: List) { val key = url.host val existing = loadForRequest(url).toMutableList() - existing.removeAll { e -> cookies.any { it.name == e.name } } + existing.removeAll { e -> cookies.any { it.name == e.name && it.domain == e.domain && it.path == e.path } } existing.addAll(cookies) - val serialized = existing.joinToString(";") { it.toString() } + val serialized = existing.joinToString("\n") { it.toString() } prefs.edit().putString(key, serialized).apply() } override fun loadForRequest(url: HttpUrl): List { val key = url.host val stored = prefs.getString(key, null) ?: return emptyList() - return stored.split(";").mapNotNull { Cookie.parse(url, it) } + return stored.split('\n').mapNotNull { line -> Cookie.parse(url, line) } } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/AppRoot.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/AppRoot.kt index 5288526..74d3ba5 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/AppRoot.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/AppRoot.kt @@ -105,11 +105,17 @@ fun AppRoot() { composable("thread/{board}/{id}/comment") { backStack -> val board = backStack.arguments?.getString("board").orEmpty() val id = backStack.arguments?.getString("id")?.toIntOrNull() ?: 0 - AddCommentScreen(boardCode = board, threadId = id) + AddCommentScreen(boardCode = board, threadId = id, onDone = { + navController.popBackStack() + }) } composable("threads/{board}/new") { backStack -> val board = backStack.arguments?.getString("board").orEmpty() - CreateThreadScreen(boardCode = board) + CreateThreadScreen(boardCode = board, onDone = { + navController.popBackStack() + navController.popBackStack() + navController.navigate("boards") + }) } } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/AddCommentScreen.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/AddCommentScreen.kt index d2dfe6e..852193c 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/AddCommentScreen.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/AddCommentScreen.kt @@ -55,9 +55,12 @@ class AddCommentVm(app: Application) : AndroidViewModel(app) { viewModelScope.launch { val settings = SettingsStore(getApplication()).data.first() val files = withContext(Dispatchers.IO) { uris.mapNotNull { uri -> ImageUtil.downscaleJpeg(context, uri) } } - runCatching { api.addComment(board, id, text, files, passcode = settings.passcode, authKey = settings.key) } - .onSuccess { onDone() } - .onFailure { error = it.message } + try { + api.addComment(board, id, text, files, passcode = settings.passcode, authKey = settings.key) + onDone() + } catch (t: Throwable) { + error = t.message + } isLoading = false withContext(Dispatchers.IO) { files.forEach { it.delete() } } } @@ -67,7 +70,7 @@ class AddCommentVm(app: Application) : AndroidViewModel(app) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AddCommentScreen(boardCode: String, threadId: Int, vm: AddCommentVm = viewModel()) { +fun AddCommentScreen(boardCode: String, threadId: Int, onDone: () -> Unit = {}, vm: AddCommentVm = viewModel()) { val context = androidx.compose.ui.platform.LocalContext.current var text by remember { mutableStateOf("") } var picked by remember { mutableStateOf>(emptyList()) } @@ -89,7 +92,7 @@ fun AddCommentScreen(boardCode: String, threadId: Int, vm: AddCommentVm = viewMo } } if (vm.error != null) Text(vm.error ?: "") - Button(onClick = { vm.send(context, boardCode, threadId, text, picked) {} }, enabled = text.isNotEmpty() && !vm.isLoading, modifier = Modifier.padding(top = 8.dp)) { Text(if (vm.isLoading) "Отправка..." else "Добавить") } + Button(onClick = { vm.send(context, boardCode, threadId, text, picked, onDone) }, enabled = text.isNotEmpty() && !vm.isLoading, modifier = Modifier.padding(top = 8.dp)) { Text(if (vm.isLoading) "Отправка..." else "Добавить") } } } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/CreateThreadScreen.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/CreateThreadScreen.kt index 6ba51e8..06f924d 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/CreateThreadScreen.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/CreateThreadScreen.kt @@ -53,9 +53,12 @@ class CreateThreadVm(app: Application) : AndroidViewModel(app) { viewModelScope.launch { val settings = SettingsStore(getApplication()).data.first() val files = withContext(Dispatchers.IO) { uris.mapNotNull { uri -> ImageUtil.downscaleJpeg(context, uri) } } - runCatching { api.createThread(board, title, text, files, passcode = settings.passcode, authKey = settings.key) } - .onSuccess { onDone() } - .onFailure { error = it.message } + try { + api.createThread(board, title, text, files, passcode = settings.passcode, authKey = settings.key) + onDone() + } catch (t: Throwable) { + error = t.message + } isLoading = false withContext(Dispatchers.IO) { files.forEach { it.delete() } } } @@ -65,7 +68,7 @@ class CreateThreadVm(app: Application) : AndroidViewModel(app) { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CreateThreadScreen(boardCode: String, vm: CreateThreadVm = viewModel()) { +fun CreateThreadScreen(boardCode: String, onDone: () -> Unit = {}, vm: CreateThreadVm = viewModel()) { val context = androidx.compose.ui.platform.LocalContext.current var title by remember { mutableStateOf("") } var text by remember { mutableStateOf("") } @@ -94,7 +97,7 @@ fun CreateThreadScreen(boardCode: String, vm: CreateThreadVm = viewModel()) { } } if (vm.error != null) Text(vm.error ?: "") - Button(onClick = { vm.create(context, boardCode, title, text, picked) {} }, enabled = title.isNotEmpty() && text.isNotEmpty() && !vm.isLoading, modifier = Modifier.padding(top = 8.dp)) { Text(if (vm.isLoading) "Отправка..." else "Создать") } + Button(onClick = { vm.create(context, boardCode, title, text, picked, onDone) }, enabled = title.isNotEmpty() && text.isNotEmpty() && !vm.isLoading, modifier = Modifier.padding(top = 8.dp)) { Text(if (vm.isLoading) "Отправка..." else "Создать") } } } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/SettingsScreen.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/SettingsScreen.kt index 524dc8a..a597712 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/SettingsScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.runtime.LaunchedEffect import androidx.lifecycle.viewModelScope @@ -60,11 +62,19 @@ class SettingsVm(app: Application) : AndroidViewModel(app) { @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(vm: SettingsVm = viewModel()) { + val scope = androidx.compose.runtime.rememberCoroutineScope() val settings by vm.flow.collectAsState(initial = SettingsData()) val subs by vm.subsFlow.collectAsState(initial = emptySet()) LaunchedEffect(Unit) { vm.loadBoards() } Scaffold(topBar = { TopAppBar(title = { Text("Настройки") }) }) { padding -> - Column(Modifier.fillMaxSize().padding(padding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Column( + Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { Row(Modifier.fillMaxWidth()) { Text("Показывать файлы", modifier = Modifier.weight(1f)) Checkbox(checked = settings.showFiles, onCheckedChange = { vm.update { it.copy(showFiles = it.showFiles.not()) } }) @@ -106,8 +116,8 @@ fun SettingsScreen(vm: SettingsVm = viewModel()) { if (vm.boards.isEmpty()) { Text("Загрузка...") } else { - LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) { - items(vm.boards) { b -> + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + vm.boards.forEach { b -> Row(Modifier.fillMaxWidth()) { Text("/${'$'}{b.code}/ ${'$'}{b.description}", modifier = Modifier.weight(1f)) val on = subs.contains(b.code) @@ -141,7 +151,14 @@ fun SettingsScreen(vm: SettingsVm = viewModel()) { Text("Принудительно оффлайн", modifier = Modifier.weight(1f)) Checkbox(checked = settings.offlineMode, onCheckedChange = { vm.update { it.copy(offlineMode = it.offlineMode.not()) } }) } - Spacer(Modifier.weight(1f)) + val ctx = androidx.compose.ui.platform.LocalContext.current + Row(Modifier.fillMaxWidth()) { + Button(onClick = { + scope.launch { com.mkfunproj.mobilemkch.data.Cache(ctx).clearAll() } + }) { Text("Очистить кэш API") } + Spacer(Modifier.weight(1f)) + Button(onClick = { /* Coil кэш чистить можно при необходимости */ }) { Text("Очистить кэш изображ.") } + } } } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadDetailScreen.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadDetailScreen.kt index d3b1930..6a14225 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadDetailScreen.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadDetailScreen.kt @@ -25,10 +25,15 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.compose.ui.platform.LocalLifecycleOwner import com.mkfunproj.mobilemkch.data.Comment import com.mkfunproj.mobilemkch.data.Repository import com.mkfunproj.mobilemkch.data.ThreadDetail @@ -71,8 +76,18 @@ fun ThreadDetailScreen( vm: ThreadDetailVm = viewModel(), onAddComment: () -> Unit = {} ) { - val scope = androidx.compose.runtime.rememberCoroutineScope() + val scope = rememberCoroutineScope() LaunchedEffect(threadId) { vm.load(boardCode, threadId) } + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner, boardCode, threadId) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + scope.launch { vm.load(boardCode, threadId) } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } Scaffold( topBar = { TopAppBar(title = { Text("#$threadId") }, actions = { androidx.compose.material3.TextButton(onClick = { scope.launch { vm.load(boardCode, threadId) } }) { Text("Обновить") } }) }, floatingActionButton = { FloatingActionButton(onClick = onAddComment) { Icon(Icons.Default.Add, contentDescription = "Add") } } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadsScreen.kt b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadsScreen.kt index 7958172..61f5538 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadsScreen.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/ui/screens/ThreadsScreen.kt @@ -95,11 +95,18 @@ fun ThreadsScreen( onCreateThread: (boardCode: String) -> Unit = {}, vm: ThreadsVm = viewModel() ) { - LaunchedEffect(boardCode) { vm.load(boardCode) } + val context = androidx.compose.ui.platform.LocalContext.current + LaunchedEffect(boardCode) { + vm.load(boardCode) + vm.viewModelScope.launch { com.mkfunproj.mobilemkch.data.SettingsStore(context).update { it.copy(lastBoard = boardCode) } } + } var currentPage by androidx.compose.runtime.remember { mutableStateOf(0) } val scope = androidx.compose.runtime.rememberCoroutineScope() Scaffold(topBar = { TopAppBar(title = { Text("/$boardCode/ — $boardDescription") }, actions = { - androidx.compose.material3.TextButton(onClick = { scope.launch { vm.load(boardCode) } }) { Text("Обновить") } + androidx.compose.material3.TextButton(onClick = { + currentPage = 0 + scope.launch { vm.load(boardCode) } + }) { Text("Обновить") } androidx.compose.material3.TextButton(onClick = { onCreateThread(boardCode) }) { Text("Создать") } }) }) { padding -> when { @@ -189,7 +196,6 @@ private fun ThreadList(padding: PaddingValues, vm: ThreadsVm, boardCode: String, if (vm.showFiles && t.files.isNotEmpty()) { Icon(Icons.Filled.AttachFile, contentDescription = null, tint = MaterialTheme.colorScheme.primary); Spacer(Modifier.width(4.dp)); Text("${t.files.size}", style = MaterialTheme.typography.labelSmall) } } if (t.text.isNotEmpty()) Text(t.text, style = MaterialTheme.typography.bodyMedium, maxLines = 3) - if (vm.showFiles && t.files.isNotEmpty()) { Spacer(Modifier.height(4.dp)); t.files.forEach { f -> AsyncImage(model = "https://mkch.pooziqo.xyz$f", contentDescription = null) } } } Divider(Modifier.padding(top = 8.dp)) } diff --git a/app/src/main/java/com/mkfunproj/mobilemkch/workers/ThreadsCheckWorker.kt b/app/src/main/java/com/mkfunproj/mobilemkch/workers/ThreadsCheckWorker.kt index 81841b0..05c1300 100644 --- a/app/src/main/java/com/mkfunproj/mobilemkch/workers/ThreadsCheckWorker.kt +++ b/app/src/main/java/com/mkfunproj/mobilemkch/workers/ThreadsCheckWorker.kt @@ -18,12 +18,13 @@ class ThreadsCheckWorker(appContext: Context, workerParams: WorkerParameters) : val api = ApiClient(applicationContext) var notified = 0 subs.forEach { board -> - runCatching { api.checkNewThreads(board) }.onSuccess { list -> + try { + val list = api.checkNewThreads(board) list.forEach { t -> NotificationHelper.notifyThread(applicationContext, board, t.id, t.title) notified += 1 } - } + } catch (_: Throwable) {} } return Result.success() }