завел коммы
This commit is contained in:
parent
7a5d6bbaf8
commit
875aab1185
@ -16,6 +16,9 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ApiClient(context: Context) {
|
class ApiClient(context: Context) {
|
||||||
private val baseUrl = "https://mkch.pooziqo.xyz"
|
private val baseUrl = "https://mkch.pooziqo.xyz"
|
||||||
@ -24,6 +27,7 @@ class ApiClient(context: Context) {
|
|||||||
|
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
private val prefs = context.getSharedPreferences("api_prefs", Context.MODE_PRIVATE)
|
private val prefs = context.getSharedPreferences("api_prefs", Context.MODE_PRIVATE)
|
||||||
|
private val cache = Cache(context)
|
||||||
|
|
||||||
private val userAgentInterceptor = Interceptor { chain ->
|
private val userAgentInterceptor = Interceptor { chain ->
|
||||||
val request = chain.request().newBuilder()
|
val request = chain.request().newBuilder()
|
||||||
@ -61,63 +65,78 @@ class ApiClient(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBoards(): List<Board> = parseBoards(get("${apiUrl}boards/"))
|
suspend fun getBoards(): List<Board> = withContext(Dispatchers.IO) {
|
||||||
|
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun authenticate(authKey: String) {
|
suspend fun getThreads(boardCode: String): List<ThreadItem> = withContext(Dispatchers.IO) {
|
||||||
if (authKey.isEmpty()) return
|
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 url = "$baseUrl/key/auth/"
|
||||||
val formHtml = get(url)
|
val formHtml = get(url)
|
||||||
val csrf = extractCsrf(formHtml)
|
val csrf = extractCsrf(formHtml)
|
||||||
val body = "csrfmiddlewaretoken=${'$'}csrf&key=${'$'}authKey"
|
val body = FormBody.Builder()
|
||||||
.toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
.add("csrfmiddlewaretoken", csrf)
|
||||||
|
.add("key", authKey)
|
||||||
|
.build()
|
||||||
val req = Request.Builder().url(url)
|
val req = Request.Builder().url(url)
|
||||||
.post(body)
|
.post(body)
|
||||||
.header("Referer", url)
|
.header("Referer", url)
|
||||||
.build()
|
.build()
|
||||||
httpClient.newCall(req).execute().use { resp ->
|
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) {
|
suspend fun loginWithPasscode(passcode: String) = withContext(Dispatchers.IO) {
|
||||||
if (passcode.isEmpty()) return
|
if (passcode.isEmpty()) return@withContext
|
||||||
val url = "$baseUrl/passcode/enter/"
|
val url = "$baseUrl/passcode/enter/"
|
||||||
val formHtml = get(url)
|
val formHtml = get(url)
|
||||||
val csrf = extractCsrf(formHtml)
|
val csrf = extractCsrf(formHtml)
|
||||||
val body = "csrfmiddlewaretoken=${'$'}csrf&passcode=${'$'}passcode"
|
val body = FormBody.Builder()
|
||||||
.toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
.add("csrfmiddlewaretoken", csrf)
|
||||||
|
.add("passcode", passcode)
|
||||||
|
.build()
|
||||||
val req = Request.Builder().url(url)
|
val req = Request.Builder().url(url)
|
||||||
.post(body)
|
.post(body)
|
||||||
.header("Referer", url)
|
.header("Referer", url)
|
||||||
.build()
|
.build()
|
||||||
httpClient.newCall(req).execute().use { resp ->
|
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 (authKey.isNotEmpty()) authenticate(authKey)
|
||||||
if (passcode.isNotEmpty()) loginWithPasscode(passcode)
|
if (passcode.isNotEmpty()) loginWithPasscode(passcode)
|
||||||
val formUrl = "$baseUrl/boards/board/$boardCode/new"
|
val formUrl = "$baseUrl/boards/board/$boardCode/new"
|
||||||
val csrf = fetchCsrf(formUrl)
|
val csrf = fetchCsrf(formUrl)
|
||||||
val req = if (files.isEmpty()) {
|
val req = if (files.isEmpty()) {
|
||||||
val body = "csrfmiddlewaretoken=$csrf&title=$title&text=$text"
|
val body = FormBody.Builder()
|
||||||
.toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
.add("csrfmiddlewaretoken", csrf)
|
||||||
|
.add("title", title)
|
||||||
|
.add("text", text)
|
||||||
|
.build()
|
||||||
Request.Builder().url(formUrl).post(body)
|
Request.Builder().url(formUrl).post(body)
|
||||||
|
.header("Referer", formUrl)
|
||||||
|
.build()
|
||||||
} else {
|
} else {
|
||||||
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("csrfmiddlewaretoken", csrf)
|
.addFormDataPart("csrfmiddlewaretoken", csrf)
|
||||||
@ -128,23 +147,28 @@ class ApiClient(context: Context) {
|
|||||||
builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb)
|
builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb)
|
||||||
}
|
}
|
||||||
Request.Builder().url(formUrl).post(builder.build())
|
Request.Builder().url(formUrl).post(builder.build())
|
||||||
|
.header("Referer", formUrl)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
.header("Referer", formUrl)
|
|
||||||
.build()
|
|
||||||
httpClient.newCall(req).execute().use { resp ->
|
httpClient.newCall(req).execute().use { resp ->
|
||||||
if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("createThread failed ${resp.code}")
|
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 (authKey.isNotEmpty()) authenticate(authKey)
|
||||||
if (passcode.isNotEmpty()) loginWithPasscode(passcode)
|
if (passcode.isNotEmpty()) loginWithPasscode(passcode)
|
||||||
val formUrl = "$baseUrl/boards/board/$boardCode/thread/$threadId/comment"
|
val formUrl = "$baseUrl/boards/board/$boardCode/thread/$threadId/comment"
|
||||||
val csrf = fetchCsrf(formUrl)
|
val csrf = fetchCsrf(formUrl)
|
||||||
val req = if (files.isEmpty()) {
|
val req = if (files.isEmpty()) {
|
||||||
val body = "csrfmiddlewaretoken=$csrf&text=$text"
|
val body = FormBody.Builder()
|
||||||
.toRequestBody("application/x-www-form-urlencoded".toMediaType())
|
.add("csrfmiddlewaretoken", csrf)
|
||||||
|
.add("text", text)
|
||||||
|
.build()
|
||||||
Request.Builder().url(formUrl).post(body)
|
Request.Builder().url(formUrl).post(body)
|
||||||
|
.header("Referer", formUrl)
|
||||||
|
.build()
|
||||||
} else {
|
} else {
|
||||||
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
val builder = MultipartBody.Builder().setType(MultipartBody.FORM)
|
||||||
.addFormDataPart("csrfmiddlewaretoken", csrf)
|
.addFormDataPart("csrfmiddlewaretoken", csrf)
|
||||||
@ -154,11 +178,12 @@ class ApiClient(context: Context) {
|
|||||||
builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb)
|
builder.addFormDataPart("files", "photo_${idx + 1}.jpg", rb)
|
||||||
}
|
}
|
||||||
Request.Builder().url(formUrl).post(builder.build())
|
Request.Builder().url(formUrl).post(builder.build())
|
||||||
|
.header("Referer", formUrl)
|
||||||
|
.build()
|
||||||
}
|
}
|
||||||
.header("Referer", formUrl)
|
|
||||||
.build()
|
|
||||||
httpClient.newCall(req).execute().use { resp ->
|
httpClient.newCall(req).execute().use { resp ->
|
||||||
if (!(resp.code == 200 || resp.code == 302)) throw RuntimeException("addComment failed ${resp.code}")
|
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 current = getThreads(boardCode)
|
||||||
val key = "savedThreads_${'$'}boardCode"
|
val key = "savedThreads_${boardCode}"
|
||||||
val savedRaw = prefs.getString(key, null)
|
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 saved = runCatching { json.decodeFromString<List<ThreadItem>>(savedRaw) }.getOrElse { emptyList() }
|
||||||
val savedIds = saved.map { it.id }.toSet()
|
val savedIds = saved.map { it.id }.toSet()
|
||||||
val newOnes = current.filter { it.id !in savedIds }
|
val newOnes = current.filter { it.id !in savedIds }
|
||||||
|
|||||||
@ -39,6 +39,10 @@ class Cache(private val context: Context) {
|
|||||||
File(dir, safeName(key)).delete()
|
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"
|
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>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
val key = url.host
|
val key = url.host
|
||||||
val existing = loadForRequest(url).toMutableList()
|
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)
|
existing.addAll(cookies)
|
||||||
val serialized = existing.joinToString(";") { it.toString() }
|
val serialized = existing.joinToString("\n") { it.toString() }
|
||||||
prefs.edit().putString(key, serialized).apply()
|
prefs.edit().putString(key, serialized).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||||
val key = url.host
|
val key = url.host
|
||||||
val stored = prefs.getString(key, null) ?: return emptyList()
|
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 ->
|
composable("thread/{board}/{id}/comment") { backStack ->
|
||||||
val board = backStack.arguments?.getString("board").orEmpty()
|
val board = backStack.arguments?.getString("board").orEmpty()
|
||||||
val id = backStack.arguments?.getString("id")?.toIntOrNull() ?: 0
|
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 ->
|
composable("threads/{board}/new") { backStack ->
|
||||||
val board = backStack.arguments?.getString("board").orEmpty()
|
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 {
|
viewModelScope.launch {
|
||||||
val settings = SettingsStore(getApplication()).data.first()
|
val settings = SettingsStore(getApplication()).data.first()
|
||||||
val files = withContext(Dispatchers.IO) { uris.mapNotNull { uri -> ImageUtil.downscaleJpeg(context, uri) } }
|
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) }
|
try {
|
||||||
.onSuccess { onDone() }
|
api.addComment(board, id, text, files, passcode = settings.passcode, authKey = settings.key)
|
||||||
.onFailure { error = it.message }
|
onDone()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
error = t.message
|
||||||
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
withContext(Dispatchers.IO) { files.forEach { it.delete() } }
|
withContext(Dispatchers.IO) { files.forEach { it.delete() } }
|
||||||
}
|
}
|
||||||
@ -67,7 +70,7 @@ class AddCommentVm(app: Application) : AndroidViewModel(app) {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@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
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
var picked by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
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 ?: "")
|
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 {
|
viewModelScope.launch {
|
||||||
val settings = SettingsStore(getApplication()).data.first()
|
val settings = SettingsStore(getApplication()).data.first()
|
||||||
val files = withContext(Dispatchers.IO) { uris.mapNotNull { uri -> ImageUtil.downscaleJpeg(context, uri) } }
|
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) }
|
try {
|
||||||
.onSuccess { onDone() }
|
api.createThread(board, title, text, files, passcode = settings.passcode, authKey = settings.key)
|
||||||
.onFailure { error = it.message }
|
onDone()
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
error = t.message
|
||||||
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
withContext(Dispatchers.IO) { files.forEach { it.delete() } }
|
withContext(Dispatchers.IO) { files.forEach { it.delete() } }
|
||||||
}
|
}
|
||||||
@ -65,7 +68,7 @@ class CreateThreadVm(app: Application) : AndroidViewModel(app) {
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@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
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
var title by remember { mutableStateOf("") }
|
var title by remember { mutableStateOf("") }
|
||||||
var text 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 ?: "")
|
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.material3.TopAppBar
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@ -60,11 +62,19 @@ class SettingsVm(app: Application) : AndroidViewModel(app) {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(vm: SettingsVm = viewModel()) {
|
fun SettingsScreen(vm: SettingsVm = viewModel()) {
|
||||||
|
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
||||||
val settings by vm.flow.collectAsState(initial = SettingsData())
|
val settings by vm.flow.collectAsState(initial = SettingsData())
|
||||||
val subs by vm.subsFlow.collectAsState(initial = emptySet())
|
val subs by vm.subsFlow.collectAsState(initial = emptySet())
|
||||||
LaunchedEffect(Unit) { vm.loadBoards() }
|
LaunchedEffect(Unit) { vm.loadBoards() }
|
||||||
Scaffold(topBar = { TopAppBar(title = { Text("Настройки") }) }) { padding ->
|
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()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Text("Показывать файлы", modifier = Modifier.weight(1f))
|
Text("Показывать файлы", modifier = Modifier.weight(1f))
|
||||||
Checkbox(checked = settings.showFiles, onCheckedChange = { vm.update { it.copy(showFiles = it.showFiles.not()) } })
|
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()) {
|
if (vm.boards.isEmpty()) {
|
||||||
Text("Загрузка...")
|
Text("Загрузка...")
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
items(vm.boards) { b ->
|
vm.boards.forEach { b ->
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
Text("/${'$'}{b.code}/ ${'$'}{b.description}", modifier = Modifier.weight(1f))
|
Text("/${'$'}{b.code}/ ${'$'}{b.description}", modifier = Modifier.weight(1f))
|
||||||
val on = subs.contains(b.code)
|
val on = subs.contains(b.code)
|
||||||
@ -141,7 +151,14 @@ fun SettingsScreen(vm: SettingsVm = viewModel()) {
|
|||||||
Text("Принудительно оффлайн", modifier = Modifier.weight(1f))
|
Text("Принудительно оффлайн", modifier = Modifier.weight(1f))
|
||||||
Checkbox(checked = settings.offlineMode, onCheckedChange = { vm.update { it.copy(offlineMode = it.offlineMode.not()) } })
|
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("Очистить кэш изображ.") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,10 +25,15 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.Comment
|
||||||
import com.mkfunproj.mobilemkch.data.Repository
|
import com.mkfunproj.mobilemkch.data.Repository
|
||||||
import com.mkfunproj.mobilemkch.data.ThreadDetail
|
import com.mkfunproj.mobilemkch.data.ThreadDetail
|
||||||
@ -71,8 +76,18 @@ fun ThreadDetailScreen(
|
|||||||
vm: ThreadDetailVm = viewModel(),
|
vm: ThreadDetailVm = viewModel(),
|
||||||
onAddComment: () -> Unit = {}
|
onAddComment: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
LaunchedEffect(threadId) { vm.load(boardCode, threadId) }
|
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(
|
Scaffold(
|
||||||
topBar = { TopAppBar(title = { Text("#$threadId") }, actions = { androidx.compose.material3.TextButton(onClick = { scope.launch { vm.load(boardCode, threadId) } }) { Text("Обновить") } }) },
|
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") } }
|
floatingActionButton = { FloatingActionButton(onClick = onAddComment) { Icon(Icons.Default.Add, contentDescription = "Add") } }
|
||||||
|
|||||||
@ -95,11 +95,18 @@ fun ThreadsScreen(
|
|||||||
onCreateThread: (boardCode: String) -> Unit = {},
|
onCreateThread: (boardCode: String) -> Unit = {},
|
||||||
vm: ThreadsVm = viewModel()
|
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) }
|
var currentPage by androidx.compose.runtime.remember { mutableStateOf(0) }
|
||||||
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
||||||
Scaffold(topBar = { TopAppBar(title = { Text("/$boardCode/ — $boardDescription") }, actions = {
|
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("Создать") }
|
androidx.compose.material3.TextButton(onClick = { onCreateThread(boardCode) }) { Text("Создать") }
|
||||||
}) }) { padding ->
|
}) }) { padding ->
|
||||||
when {
|
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 (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 (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))
|
Divider(Modifier.padding(top = 8.dp))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,12 +18,13 @@ class ThreadsCheckWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||||||
val api = ApiClient(applicationContext)
|
val api = ApiClient(applicationContext)
|
||||||
var notified = 0
|
var notified = 0
|
||||||
subs.forEach { board ->
|
subs.forEach { board ->
|
||||||
runCatching { api.checkNewThreads(board) }.onSuccess { list ->
|
try {
|
||||||
|
val list = api.checkNewThreads(board)
|
||||||
list.forEach { t ->
|
list.forEach { t ->
|
||||||
NotificationHelper.notifyThread(applicationContext, board, t.id, t.title)
|
NotificationHelper.notifyThread(applicationContext, board, t.id, t.title)
|
||||||
notified += 1
|
notified += 1
|
||||||
}
|
}
|
||||||
}
|
} catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user