From cef526e77852c90c48287657372c19b812220999 Mon Sep 17 00:00:00 2001 From: b-dev-mobile Date: Tue, 7 Jul 2026 21:47:49 +0000 Subject: [PATCH] =?UTF-8?q?=D0=AD=D1=82=D0=B0=D0=BF=200=20=D0=BF.5=20(grad?= =?UTF-8?q?le-=D0=B3=D0=B8=D0=B3=D0=B8=D0=B5=D0=BD=D0=B0):=20parallel=3Dtr?= =?UTF-8?q?ue=20+=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BC?= =?UTF-8?q?=D1=91=D1=80=D1=82=D0=B2=D1=8B=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - org.gradle.parallel=true (быстрее сборка 20 модулей) - удалены core:data, core:ui (пустые заготовки) и feature:widgets (не в settings.gradle, не компилировался) + чистка settings.gradle - nonTransitiveRClass=true и убирание enableJetifier ОТКАЧЕНЫ: vendor-форк talk-android их не переживает (R транзитивно; legacy android.support.* в зависимостях типа FlexibleAdapter). Причины — в комментах gradle.properties. Проверено сборкой: assembleRelease BUILD SUCCESSFUL. Этап 0 закрыт. --- CHANGELOG.md | 1 + core/data/build.gradle | 26 -- core/data/src/main/AndroidManifest.xml | 2 - core/ui/build.gradle | 3 - feature/widgets/build.gradle | 41 --- feature/widgets/src/main/AndroidManifest.xml | 2 - .../feature/widgets/WidgetsRepository.kt | 271 ------------------ .../f7cloud/feature/widgets/WidgetsScreen.kt | 176 ------------ .../f7cloud/feature/widgets/WidgetsUiState.kt | 12 - .../feature/widgets/WidgetsViewModel.kt | 79 ----- gradle.properties | 7 +- settings.gradle | 2 - 12 files changed, 7 insertions(+), 615 deletions(-) delete mode 100644 core/data/build.gradle delete mode 100644 core/data/src/main/AndroidManifest.xml delete mode 100644 core/ui/build.gradle delete mode 100644 feature/widgets/build.gradle delete mode 100644 feature/widgets/src/main/AndroidManifest.xml delete mode 100644 feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsRepository.kt delete mode 100644 feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsScreen.kt delete mode 100644 feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsUiState.kt delete mode 100644 feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsViewModel.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 44dad8c..365fd09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск` +- 2026-07-07 | v0.5.114 (122) | Этап 0 п.5 (gradle-гигиена): org.gradle.parallel=true (ускорение сборки 20 модулей); удалены мёртвые модули core:data, core:ui (пустые заготовки) и feature:widgets (не был в settings.gradle, не компилировался) + чистка settings.gradle. nonTransitiveRClass и убирание jetifier — ПРОБОВАЛ, откатил: vendor-форк talk-android ломается (R транзитивно + legacy android.support.* в его depS); причины в комментах gradle.properties. Проверено сборкой: assembleRelease BUILD SUCCESSFUL. **Этап 0 завершён** (кроме п.4c — публикация исходников, к CI этапа 1) | контракты не менялись | низкий: удалён неиспользуемый код, ускорена сборка - 2026-07-07 | v0.5.114 (122) | Требование владельца: звонки/push ДОЛЖНЫ работать при заблокированном телефоне. (1) Биометрическая привязка app password ОТКЛОНЕНА (вариант А) — она неустранимо блокировала приём звонка без разблокировки; откачена из main, сохранена на ветке wip/biometric-binding. App password остаётся защищён шифрованием в покое (Keystore AES-256-GCM из п.3, работает в фоне → звонки/push/приём при блокировке ОК). (2) Приватность локскрина: уведомления сообщений и звонков → VISIBILITY_PRIVATE + generic public-версия («Новое сообщение»/«Входящий звонок»), канал lockscreenVisibility=PRIVATE. Полный контент (отправитель/текст/имя звонящего) — только после разблокировки. Push у приложения plaintext, пароль для показа/звонка НЕ нужен. Проверено сборкой: assembleRelease 53 МБ | контракты не менялись | низкий: только UX уведомлений; звонки/push в фоне работают как раньше - 2026-07-07 | v0.5.114 (122) | Этап 0 п.4b (GPL): восстановлены затёртые копирайты Nextcloud в vendor/talk-android — точной сверкой с апстримом v23.0.0 (скачан с GitHub). 102 файла: искажённые email реальных разработчиков NC (@f7cloud.com → настоящие), холдер «F7cloud»/«F7cloud and F7cloud contributors» → «Nextcloud GmbH and Nextcloud contributors». Название продукта в заголовках оставлено «F7cloud Talk» (ребренд форка, требование trademark-политики NC). Менялись ТОЛЬКО строки копирайта в комментах, код не тронут (103+/103−). Проверено сборкой: assembleRelease 53 МБ OK | лицензирование/атрибуция (не код) | нет: функционально нейтрально, снимает нарушение GPLv3 §4 (атрибуция) - 2026-07-07 | v0.5.114 (122) | Этап 0 п.4a (GPL): добавлен LICENSE (GPL-3.0-or-later) в корень репо — приложение производно от vendored Nextcloud Talk (GPL), значит весь APK под GPL; раздел «Лицензия» в README (обязательство corresponding source по тегу релиза). Восстановление затёртых копирайтов Nextcloud в vendor (п.4b) — отдельно, сверкой с апстримом v23.0.0 | лицензирование (не код) | нет diff --git a/core/data/build.gradle b/core/data/build.gradle deleted file mode 100644 index 4572494..0000000 --- a/core/data/build.gradle +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'ru.forbion.f7cloud.core.data' - compileSdk 36 - - defaultConfig { - minSdk 26 - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } -} - -dependencies { - implementation project(':core:auth') - implementation project(':core:database') - implementation project(':core:network') -} diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml deleted file mode 100644 index 8072ee0..0000000 --- a/core/data/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/core/ui/build.gradle b/core/ui/build.gradle deleted file mode 100644 index 59a20ef..0000000 --- a/core/ui/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id 'java-library' -} diff --git a/feature/widgets/build.gradle b/feature/widgets/build.gradle deleted file mode 100644 index 6c15602..0000000 --- a/feature/widgets/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' - id 'org.jetbrains.kotlin.plugin.compose' -} - -android { - namespace 'ru.forbion.f7cloud.feature.widgets' - compileSdk 36 - - defaultConfig { - minSdk 26 - } - buildFeatures { - compose true - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - kotlinOptions { - jvmTarget = '17' - } -} - -dependencies { - implementation project(':core:auth') - implementation project(':core:network') - implementation project(':core:designsystem') - implementation project(':feature:files') - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' - implementation 'org.json:json:20240303' - def composeBom = platform('androidx.compose:compose-bom:2025.02.00') - implementation composeBom - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.material3:material3' - implementation 'androidx.compose.foundation:foundation' - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.1' - implementation 'io.coil-kt:coil-compose:2.6.0' - implementation 'io.coil-kt:coil-svg:2.6.0' -} diff --git a/feature/widgets/src/main/AndroidManifest.xml b/feature/widgets/src/main/AndroidManifest.xml deleted file mode 100644 index 8072ee0..0000000 --- a/feature/widgets/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsRepository.kt b/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsRepository.kt deleted file mode 100644 index 2fb48f7..0000000 --- a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsRepository.kt +++ /dev/null @@ -1,271 +0,0 @@ -package ru.forbion.f7cloud.feature.widgets - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.Request -import org.json.JSONArray -import org.json.JSONObject -import ru.forbion.f7cloud.core.auth.AuthSession -import ru.forbion.f7cloud.core.network.NetworkFactory -import ru.forbion.f7cloud.core.network.UnauthorizedException - -class WidgetsRepository { - suspend fun loadDashboard(session: AuthSession): List = coroutineScope { - val client = NetworkFactory.newAuthedClient( - session.username, - session.appPassword, - session.trustAllCerts, - ) - val base = "${session.serverUrl.trimEnd('/')}/ocs/v2.php/apps/dashboard" - - val widgetsDeferred = async(Dispatchers.IO) { - parseWidgets(getOcsData(client, "$base/api/v1/widgets")) - } - val layoutDeferred = async(Dispatchers.IO) { - fetchLayoutWithTimeout(client, base) - } - val itemsV2Deferred = async(Dispatchers.IO) { - parseWidgetItemsV2(getOcsData(client, "$base/api/v2/widget-items?limit=7")) - } - // recommendations есть только в API v1 - val itemsV1Deferred = async(Dispatchers.IO) { - parseWidgetItemsV1( - getOcsData( - client, - buildWidgetItemsUrl(base, apiVersion = 1, widgetIds = listOf("recommendations")), - ), - ) - } - - val allWidgets = widgetsDeferred.await().associateBy { it.id } - val layout = layoutDeferred.await() - val itemsByWidget = mergeWidgetItems(itemsV1Deferred.await(), itemsV2Deferred.await()) - val widgetOrder = resolveWidgetOrder(layout, allWidgets, itemsByWidget.keys) - - widgetOrder.map { widget -> - val bucket = itemsByWidget[widget.id] - DashboardWidgetGroup( - id = widget.id, - title = widget.title, - iconUrl = widget.iconUrl, - items = bucket?.items.orEmpty(), - emptyMessage = bucket?.emptyMessage, - ) - } - } - - private suspend fun fetchLayoutWithTimeout( - client: okhttp3.OkHttpClient, - base: String, - ): List = withContext(Dispatchers.IO) { - withTimeoutOrNull(LAYOUT_TIMEOUT_MS) { - runCatching { - parseLayout(getOcsData(client, "$base/api/v3/layout")) - }.getOrNull() - }?.takeIf { it.isNotEmpty() } ?: DEFAULT_LAYOUT - } - - private fun buildWidgetItemsUrl( - base: String, - apiVersion: Int, - widgetIds: List = emptyList(), - ): String { - val builder = "$base/api/v$apiVersion/widget-items?limit=7" - .toHttpUrlOrNull() - ?.newBuilder() - ?: return "$base/api/v$apiVersion/widget-items?limit=7" - widgetIds.forEach { builder.addQueryParameter("widgets[]", it) } - return builder.build().toString() - } - - private fun resolveWidgetOrder( - layout: List, - allWidgets: Map, - itemWidgetIds: Set, - ): List { - val order = layout.ifEmpty { DEFAULT_LAYOUT } - val seen = mutableSetOf() - val ordered = mutableListOf() - for (id in order) { - if (!seen.add(id)) continue - val meta = allWidgets[id] ?: continue - if (id in itemWidgetIds || meta.id in allWidgets) { - ordered += meta - } - } - for (id in itemWidgetIds) { - if (!seen.add(id)) continue - allWidgets[id]?.let { ordered += it } - } - return ordered - } - - private fun mergeWidgetItems( - v1: Map, - v2: Map, - ): Map { - val ids = (v1.keys + v2.keys).toSet() - return ids.associateWith { id -> - val fromV2 = v2[id] - val fromV1 = v1[id] - when { - fromV2 != null && fromV2.items.isNotEmpty() -> fromV2 - fromV1 != null && fromV1.items.isNotEmpty() -> fromV1 - fromV2 != null -> fromV2 - else -> fromV1 ?: WidgetItemsBucket() - } - } - } - - private fun getOcsData(client: okhttp3.OkHttpClient, url: String): Any { - val jsonUrl = if (url.contains("format=")) url else { - if (url.contains("?")) "$url&format=json" else "$url?format=json" - } - val request = Request.Builder() - .url(jsonUrl) - .header("OCS-APIRequest", "true") - .header("Accept", "application/json") - .build() - client.newCall(request).execute().use { response -> - if (response.code == 401) throw UnauthorizedException() - if (!response.isSuccessful || response.body == null) { - error("Dashboard API HTTP ${response.code}") - } - val body = response.body!!.string() - val ocs = JSONObject(body).optJSONObject("ocs") - ?: error("Некорректный ответ Dashboard API") - val meta = ocs.optJSONObject("meta") - val status = meta?.optString("status").orEmpty() - if (status.equals("failure", ignoreCase = true)) { - error(meta?.optString("message").orEmpty().ifBlank { "Dashboard API error" }) - } - return ocs.opt("data") ?: JSONObject() - } - } - - private fun parseLayout(data: Any): List { - if (data !is JSONObject) return emptyList() - val layout = data.optJSONArray("layout") ?: return emptyList() - return buildList { - for (i in 0 until layout.length()) { - val id = layout.optString(i) - if (id.isNotBlank()) add(id) - } - } - } - - private fun parseWidgets(data: Any): List { - val out = mutableListOf() - when (data) { - is JSONObject -> { - val keys = data.keys() - while (keys.hasNext()) { - val key = keys.next() - val obj = data.optJSONObject(key) ?: continue - out += widgetMetaFromJson(obj, key) - } - } - is JSONArray -> for (i in 0 until data.length()) { - val obj = data.optJSONObject(i) ?: continue - out += widgetMetaFromJson(obj, obj.optString("id")) - } - } - return out - } - - private fun widgetMetaFromJson(obj: JSONObject, fallbackId: String): WidgetMeta { - return WidgetMeta( - id = obj.optString("id", fallbackId), - title = obj.optString("title", fallbackId), - order = obj.optInt("order", 0), - iconUrl = obj.optString("icon_url"), - ) - } - - private fun parseWidgetItemsV1(data: Any): Map { - val out = mutableMapOf() - if (data !is JSONObject) return out - val keys = data.keys() - while (keys.hasNext()) { - val widgetId = keys.next() - val value = data.opt(widgetId) ?: continue - val items = when (value) { - is JSONArray -> parseItemArray(value) - is JSONObject -> parseItemArray(value.optJSONArray("items") ?: JSONArray()) - else -> emptyList() - } - if (items.isNotEmpty()) { - out[widgetId] = WidgetItemsBucket(items = items) - } - } - return out - } - - private fun parseWidgetItemsV2(data: Any): Map { - val out = mutableMapOf() - if (data !is JSONObject) return out - val keys = data.keys() - while (keys.hasNext()) { - val widgetId = keys.next() - val widgetObj = data.optJSONObject(widgetId) ?: continue - val items = parseItemArray(widgetObj.optJSONArray("items") ?: JSONArray()) - val emptyMessage = widgetObj.optString("emptyContentMessage") - .ifBlank { widgetObj.optString("halfEmptyContentMessage") } - .ifBlank { null } - if (items.isNotEmpty() || !emptyMessage.isNullOrBlank()) { - out[widgetId] = WidgetItemsBucket(items = items, emptyMessage = emptyMessage) - } - } - return out - } - - private fun parseItemArray(itemsArray: JSONArray): List { - val items = mutableListOf() - for (i in 0 until itemsArray.length()) { - val item = itemsArray.optJSONObject(i) ?: continue - items += WidgetItem( - title = item.optString("title"), - subtitle = item.optString("subtitle"), - link = item.optString("link"), - iconUrl = item.optString("iconUrl"), - ) - } - return items - } - - companion object { - private const val LAYOUT_TIMEOUT_MS = 2_000L - private val DEFAULT_LAYOUT = listOf("recommendations", "spreed", "mail", "calendar") - } -} - -private data class WidgetMeta( - val id: String, - val title: String, - val order: Int, - val iconUrl: String, -) - -data class WidgetItem( - val title: String, - val subtitle: String, - val link: String, - val iconUrl: String = "", -) - -data class DashboardWidgetGroup( - val id: String, - val title: String, - val iconUrl: String = "", - val items: List, - val emptyMessage: String? = null, -) - -private data class WidgetItemsBucket( - val items: List = emptyList(), - val emptyMessage: String? = null, -) diff --git a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsScreen.kt b/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsScreen.kt deleted file mode 100644 index afa115a..0000000 --- a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsScreen.kt +++ /dev/null @@ -1,176 +0,0 @@ -package ru.forbion.f7cloud.feature.widgets - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage -import ru.forbion.f7cloud.core.auth.AuthSession -import ru.forbion.f7cloud.core.designsystem.F7Colors -import ru.forbion.f7cloud.core.designsystem.F7ListCard -import ru.forbion.f7cloud.core.designsystem.F7ModuleScreen -import ru.forbion.f7cloud.feature.files.OfficeEditorLaunch -import ru.forbion.f7cloud.feature.files.OfficeFileLinks -import ru.forbion.f7cloud.feature.files.OfficeFiles -import ru.forbion.f7cloud.feature.files.OfficeWarmup - -@Composable -fun WidgetsScreen( - session: AuthSession, - modifier: Modifier = Modifier, - onUnauthorized: () -> Unit = {}, - onOpenOfficeEditor: (OfficeEditorLaunch) -> Unit = {}, - onOpenLink: (String) -> Unit = {}, -) { - val vm: WidgetsViewModel = viewModel() - val state by vm.state.collectAsState() - - LaunchedEffect(session.serverUrl, session.username) { - OfficeWarmup.warm(session) - vm.load(session) - } - LaunchedEffect(state.unauthorized) { - if (state.unauthorized) onUnauthorized() - } - LaunchedEffect(state.editorLaunch) { - val launch = state.editorLaunch ?: return@LaunchedEffect - onOpenOfficeEditor(launch) - vm.clearEditorLaunch() - } - - F7ModuleScreen( - modifier = modifier, - loading = state.loading && state.groups.isEmpty(), - error = state.error, - onRefresh = { vm.load(session) }, - ) { - if (!state.loading && state.groups.isEmpty() && state.error.isNullOrBlank()) { - Text( - text = "Нет виджетов на главной. Настройте их в веб-интерфейсе.", - style = MaterialTheme.typography.bodyMedium, - color = F7Colors.TextSecondary, - ) - } - if (state.openingFile != null) { - CircularProgressIndicator(color = F7Colors.Primary) - Text( - text = "Открываем «${state.openingFile}»…", - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) - } - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(10.dp), - ) { - items(state.groups, key = { it.id }) { group -> - F7ListCard { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - if (group.iconUrl.isNotBlank()) { - AsyncImage( - model = group.iconUrl, - contentDescription = group.title, - modifier = Modifier.size(24.dp), - contentScale = ContentScale.Fit, - ) - } - Text( - group.title.ifBlank { group.id }, - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.weight(1f), - ) - } - group.items.forEach { item -> - val canOpenOffice = item.link.isNotBlank() && - OfficeFileLinks.parseFileId(item.link, session.serverUrl) != null && - (OfficeFiles.isOfficeFile(item.title) || - OfficeFiles.isOfficeFile(item.link.substringAfterLast('/'))) - val canOpenLink = item.link.isNotBlank() && !canOpenOffice - Row( - modifier = Modifier - .fillMaxWidth() - .then( - when { - canOpenOffice -> Modifier.clickable { vm.openWidgetItem(session, item) } - canOpenLink -> Modifier.clickable { onOpenLink(item.link) } - else -> Modifier - }, - ) - .padding(vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - if (item.iconUrl.isNotBlank()) { - AsyncImage( - model = item.iconUrl, - contentDescription = null, - modifier = Modifier.size(32.dp), - contentScale = ContentScale.Fit, - ) - } - androidx.compose.foundation.layout.Column(modifier = Modifier.weight(1f)) { - Text( - item.title.ifBlank { "—" }, - style = MaterialTheme.typography.bodyMedium, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - if (item.subtitle.isNotBlank()) { - Text( - item.subtitle, - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - if (canOpenOffice) { - Text( - text = "Открыть в Office", - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) - } else if (canOpenLink) { - Text( - text = "Открыть", - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) - } - } - } - } - if (group.items.isEmpty()) { - Text( - text = group.emptyMessage ?: "Нет элементов", - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) - } - } - } - } - } -} diff --git a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsUiState.kt b/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsUiState.kt deleted file mode 100644 index d49abd4..0000000 --- a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ru.forbion.f7cloud.feature.widgets - -import ru.forbion.f7cloud.feature.files.OfficeEditorLaunch - -data class WidgetsUiState( - val loading: Boolean = false, - val groups: List = emptyList(), - val error: String? = null, - val unauthorized: Boolean = false, - val openingFile: String? = null, - val editorLaunch: OfficeEditorLaunch? = null, -) diff --git a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsViewModel.kt b/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsViewModel.kt deleted file mode 100644 index 231c591..0000000 --- a/feature/widgets/src/main/java/ru/forbion/f7cloud/feature/widgets/WidgetsViewModel.kt +++ /dev/null @@ -1,79 +0,0 @@ -package ru.forbion.f7cloud.feature.widgets - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import ru.forbion.f7cloud.core.auth.AuthSession -import ru.forbion.f7cloud.core.network.UnauthorizedException -import ru.forbion.f7cloud.feature.files.OfficeFileLinks -import ru.forbion.f7cloud.feature.files.OfficeFileOpener -import ru.forbion.f7cloud.feature.files.OfficeFiles - -class WidgetsViewModel( - private val repository: WidgetsRepository = WidgetsRepository(), - private val officeFileOpener: OfficeFileOpener = OfficeFileOpener(), -) : ViewModel() { - private val _state = MutableStateFlow(WidgetsUiState()) - val state: StateFlow = _state.asStateFlow() - - fun load(session: AuthSession) { - viewModelScope.launch(Dispatchers.IO) { - _state.value = _state.value.copy(loading = true, error = null) - runCatching { repository.loadDashboard(session) } - .onSuccess { groups -> - _state.value = _state.value.copy(loading = false, groups = groups) - } - .onFailure { t -> - _state.value = _state.value.copy( - loading = false, - error = t.message, - unauthorized = t is UnauthorizedException, - ) - } - } - } - - fun openWidgetItem(session: AuthSession, item: WidgetItem) { - if (item.link.isBlank()) return - val fileId = OfficeFileLinks.parseFileId(item.link, session.serverUrl) - val isOffice = OfficeFiles.isOfficeFile(item.title) || - OfficeFiles.isOfficeFile(item.link.substringAfterLast('/')) - if (fileId == null || !isOffice) { - _state.value = _state.value.copy( - error = "Открытие через Office доступно только для документов Word/Excel", - ) - return - } - viewModelScope.launch(Dispatchers.IO) { - _state.value = _state.value.copy(openingFile = item.title, error = null) - runCatching { - officeFileOpener.prepareLaunch( - session, - fileId, - OfficeFileLinks.titleFromLink(item.link, item.title), - ) - } - .onSuccess { launch -> - _state.value = _state.value.copy( - openingFile = null, - editorLaunch = launch, - ) - } - .onFailure { t -> - _state.value = _state.value.copy( - openingFile = null, - error = t.message ?: "Не удалось открыть документ", - unauthorized = t is UnauthorizedException, - ) - } - } - } - - fun clearEditorLaunch() { - _state.value = _state.value.copy(editorLaunch = null) - } -} diff --git a/gradle.properties b/gradle.properties index c32d314..f115cd8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,13 @@ org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8 -org.gradle.parallel=false +org.gradle.parallel=true org.gradle.caching=true android.useAndroidX=true +# enableJetifier ОБЯЗАТЕЛЕН: vendor-форк talk-android тянет legacy-зависимости с +# android.support.* (напр. FlexibleAdapter → android.support.v7.widget.RecyclerView) — +# без jetifier сборка падает (проверено). Убрать можно только после чистки vendor-депов. android.enableJetifier=true +# nonTransitiveRClass ОСТАВЛЕН false: vendor ссылается на R транзитивно (R.color.transparent, +# R.id.container из зависимостей) — true ломает ~сотни ссылок в форке (проверено). android.nonTransitiveRClass=false kotlin.code.style=official f7cloudAsLibrary=true diff --git a/settings.gradle b/settings.gradle index 9c2b357..054830c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,12 +19,10 @@ rootProject.name = "F7CloudMobile" include(":app") include(":core:auth") -include(":core:data") include(":core:database") include(":core:designsystem") include(":core:network") include(":core:push") -include(":core:ui") include(":feature:files") include(":feature:talk") include(":feature:talk-native")