завел коммы
This commit is contained in:
parent
7a5d6bbaf8
commit
875aab1185
@ -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<Board> = parseBoards(get("${apiUrl}boards/"))
|
||||
|
||||
fun getThreads(boardCode: String): List<ThreadItem> = 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<Comment> =
|
||||
parseComments(get("${apiUrl}board/$boardCode/thread/$threadId/comments"))
|
||||
|
||||
fun getFullThread(boardCode: String, threadId: Int): Pair<ThreadDetail, List<Comment>> {
|
||||
val detail = getThreadDetail(boardCode, threadId)
|
||||
val comments = getComments(boardCode, threadId)
|
||||
return detail to comments
|
||||
suspend fun getBoards(): List<Board> = withContext(Dispatchers.IO) {
|
||||
parseBoards(get("${apiUrl}boards/"))
|
||||
}
|
||||
|
||||
fun authenticate(authKey: String) {
|
||||
if (authKey.isEmpty()) return
|
||||
suspend fun getThreads(boardCode: String): List<ThreadItem> = 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<Comment> = withContext(Dispatchers.IO) {
|
||||
parseComments(get("${apiUrl}board/$boardCode/thread/$threadId/comments"))
|
||||
}
|
||||
|
||||
suspend fun getFullThread(boardCode: String, threadId: Int): Pair<ThreadDetail, List<Comment>> = 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<File> = emptyList(), passcode: String = "", authKey: String = "") {
|
||||
suspend fun createThread(boardCode: String, title: String, text: String, files: List<File> = 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()
|
||||
}
|
||||
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<File> = emptyList(), passcode: String = "", authKey: String = "") {
|
||||
suspend fun addComment(boardCode: String, threadId: Int, text: String, files: List<File> = 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()
|
||||
}
|
||||
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<ThreadItem> {
|
||||
suspend fun checkNewThreads(boardCode: String): List<ThreadItem> = 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<List<ThreadItem>>(savedRaw) }.getOrElse { emptyList() }
|
||||
val savedIds = saved.map { it.id }.toSet()
|
||||
val newOnes = current.filter { it.id !in savedIds }
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
|
||||
@ -11,16 +11,16 @@ class PersistentCookieJar(context: Context) : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
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<Cookie> {
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<List<Uri>>(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 "Добавить") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 "Создать") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()) } })
|
||||
}
|
||||
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("Очистить кэш изображ.") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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") } }
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user