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")