Redesign Plugins screen to match Addons screen visuals, rename from Scraper Plugins

Scrapers are now grouped under their owning repository instead of two
separate flat lists, and the screen reuses AddonStoreScreen's visual
language (icon-circle back button, bordered repo cards, icon buttons)
instead of its own bespoke plain-text styling. Adds a refreshRepository
action reusing the existing re-fetch dispatch.
This commit is contained in:
KhooLy 2026-07-20 03:15:39 +03:00
parent 1e776bbb8a
commit 28501ff31b
5 changed files with 290 additions and 209 deletions

View file

@ -133,6 +133,11 @@ class PluginRepositoryManager @Inject constructor(
persistCurrentState()
}
suspend fun refreshRepository(manifestUrl: String) {
runtime.dispatch(mapOf("type" to "pluginRepositoryAddRequested", "manifestUrl" to manifestUrl))
persistCurrentState()
}
suspend fun toggleScraper(scraperId: String, enabled: Boolean) {
runtime.dispatch(mapOf("type" to "pluginScraperToggled", "scraperId" to scraperId, "enabled" to enabled))
persistCurrentState()

View file

@ -1,10 +1,12 @@
package com.fluxa.app.ui.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -13,17 +15,24 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -33,14 +42,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Icon
import com.fluxa.app.common.AppStrings
import com.fluxa.app.plugins.PluginRepositoryUiModel
import com.fluxa.app.plugins.PluginScraperUiModel
@ -59,6 +68,7 @@ fun PluginsSettingsRoute(
language = language,
onAddRepository = viewModel::addRepository,
onRemoveRepository = viewModel::removeRepository,
onRefreshRepository = viewModel::refreshRepository,
onToggleScraper = viewModel::toggleScraper,
onOpenScraperSettings = viewModel::openScraperSettings,
onBackRequested = onBackRequested
@ -84,155 +94,233 @@ fun PluginsSettingsScreen(
language: String?,
onAddRepository: (String) -> Unit,
onRemoveRepository: (String) -> Unit,
onRefreshRepository: (String) -> Unit,
onToggleScraper: (String, Boolean) -> Unit,
onOpenScraperSettings: (PluginScraperUiModel) -> Unit,
onBackRequested: () -> Unit,
modifier: Modifier = Modifier
) {
var showAddDialog by remember { mutableStateOf(false) }
val scrapersByRepository = state.scrapers.groupBy { it.repositoryUrl }
Box(modifier = modifier.fillMaxSize().background(FluxaColors.background)) {
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 20.dp)) {
PluginsTopBar(title = AppStrings.t(language, "settings.plugins.title"), onBack = onBackRequested)
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
PluginsSectionHeader(AppStrings.t(language, "settings.plugins.repositories"))
PluginsActionRow(AppStrings.t(language, "settings.plugins.add_repository")) { showAddDialog = true }
if (state.addingRepositoryUrl != null) {
Text(
AppStrings.t(language, "settings.plugins.adding"),
color = Color.White.copy(alpha = 0.6f),
fontSize = 13.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
}
state.error?.let { error ->
Text(
AppStrings.format(language, "settings.plugins.error", error),
color = FluxaColors.errorRed,
fontSize = 13.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
}
if (state.repositories.isEmpty()) {
Text(
AppStrings.t(language, "settings.plugins.no_repositories"),
color = Color.White.copy(alpha = 0.5f),
modifier = Modifier.padding(vertical = 12.dp)
)
} else {
state.repositories.forEach { repository ->
PluginRepositoryRow(
repository = repository,
language = language,
onRemove = { onRemoveRepository(repository.manifestUrl) }
)
}
}
Spacer(Modifier.height(20.dp))
PluginsSectionHeader(AppStrings.t(language, "settings.plugins.scrapers"))
if (state.scrapers.isEmpty()) {
Text(
AppStrings.t(language, "settings.plugins.no_scrapers"),
color = Color.White.copy(alpha = 0.5f),
modifier = Modifier.padding(vertical = 12.dp)
)
} else {
state.scrapers.forEach { scraper ->
PluginScraperRow(
scraper = scraper,
language = language,
onToggle = { enabled -> onToggleScraper(scraper.id, enabled) },
onOpenSettings = { onOpenScraperSettings(scraper) }
)
}
}
Spacer(Modifier.height(120.dp))
}
}
}
if (showAddDialog) {
PluginAddRepositoryDialog(
language = language,
onDismiss = { showAddDialog = false },
onConfirm = { url ->
onAddRepository(url)
showAddDialog = false
}
)
}
}
@Composable
private fun PluginsTopBar(title: String, onBack: () -> Unit) {
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier.size(40.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.05f)).clickable(onClick = onBack),
contentAlignment = Alignment.Center
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Text("", color = Color.White)
item {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(18.dp)) {
var backFocused by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.onFocusChanged { backFocused = it.isFocused }
.background(if (backFocused) Color.White else Color.White.copy(alpha = 0.05f))
.clickable(onClick = onBackRequested),
contentAlignment = Alignment.Center
) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
tint = if (backFocused) Color.Black else Color.White,
modifier = Modifier.size(20.dp)
)
}
Text(
text = AppStrings.t(language, "settings.plugins.title"),
color = Color.White,
fontWeight = FontWeight.Black,
fontSize = 26.sp
)
}
}
item {
PluginRepositoryInput(language = language, onSubmit = onAddRepository)
}
if (state.error != null) {
item {
Text(
text = AppStrings.format(language, "settings.plugins.error", state.error),
color = FluxaColors.errorRed,
fontSize = 13.sp
)
}
}
val addingNewRepositoryUrl = state.addingRepositoryUrl
?.takeIf { url -> state.repositories.none { it.manifestUrl == url } }
if (addingNewRepositoryUrl != null) {
item {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
CircularProgressIndicator(color = Color.White.copy(alpha = 0.68f), strokeWidth = 2.dp, modifier = Modifier.size(16.dp))
Text(
text = AppStrings.t(language, "settings.plugins.adding"),
color = Color.White.copy(alpha = 0.6f),
fontSize = 13.sp
)
}
}
}
item {
Text(
text = AppStrings.t(language, "settings.plugins.installed_repositories"),
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
)
}
if (state.repositories.isEmpty()) {
item {
Text(
text = AppStrings.t(language, "settings.plugins.no_repositories"),
color = Color.White.copy(alpha = 0.5f),
fontSize = 13.sp,
modifier = Modifier.padding(vertical = 8.dp)
)
}
} else {
items(state.repositories, key = { "repo:${it.manifestUrl}" }) { repository ->
PluginRepositoryGroup(
repository = repository,
scrapers = scrapersByRepository[repository.manifestUrl].orEmpty(),
language = language,
isRefreshing = state.addingRepositoryUrl == repository.manifestUrl,
onRemove = { onRemoveRepository(repository.manifestUrl) },
onRefresh = { onRefreshRepository(repository.manifestUrl) },
onToggleScraper = onToggleScraper,
onOpenScraperSettings = onOpenScraperSettings
)
}
}
item { Spacer(Modifier.height(80.dp)) }
}
Spacer(Modifier.width(12.dp))
Text(title, color = Color.White, fontWeight = FontWeight.Black, fontSize = 22.sp)
}
}
@Composable
private fun PluginsSectionHeader(title: String) {
Text(
title.uppercase(),
color = Color.White.copy(alpha = 0.4f),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
}
@Composable
private fun PluginsActionRow(label: String, onClick: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(label, color = Color.White)
Text("+", color = Color.White.copy(alpha = 0.6f), fontSize = 18.sp)
}
}
@Composable
private fun PluginRepositoryRow(
repository: PluginRepositoryUiModel,
private fun PluginRepositoryInput(
language: String?,
onRemove: () -> Unit
onSubmit: (String) -> Unit
) {
Row(
var url by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
.clip(RoundedCornerShape(10.dp))
.background(Color.White.copy(alpha = 0.05f))
.padding(horizontal = 14.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
.clip(RoundedCornerShape(18.dp))
.background(Color.White.copy(alpha = 0.04f))
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(repository.name.ifBlank { repository.manifestUrl }, color = Color.White, fontWeight = FontWeight.Bold)
Text(
AppStrings.format(language, "settings.plugins.scraper_count", repository.scraperCount),
color = Color.White.copy(alpha = 0.5f),
fontSize = 12.sp
)
}
Text(
AppStrings.t(language, "settings.plugins.remove"),
color = FluxaColors.errorRed,
fontSize = 13.sp,
modifier = Modifier.clickable(onClick = onRemove).padding(8.dp)
OutlinedTextField(
value = url,
onValueChange = { url = it },
modifier = Modifier.fillMaxWidth().height(52.dp),
singleLine = true,
placeholder = {
Text(
AppStrings.t(language, "settings.plugins.repository_url_placeholder"),
color = Color.White.copy(alpha = 0.3f),
fontSize = 13.sp
)
},
colors = OutlinedTextFieldDefaults.colors(
unfocusedBorderColor = Color.White.copy(alpha = 0.1f),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
cursorColor = Color.White,
focusedContainerColor = Color.Black.copy(alpha = 0.2f),
unfocusedContainerColor = Color.Black.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(12.dp)
)
androidx.compose.material3.Button(
onClick = {
val trimmed = url.trim()
if (trimmed.isNotEmpty()) {
onSubmit(trimmed)
url = ""
}
},
enabled = url.isNotBlank(),
modifier = Modifier.fillMaxWidth().height(44.dp),
shape = RoundedCornerShape(12.dp),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = Color.Black)
) {
Icon(Icons.Filled.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(AppStrings.t(language, "settings.plugins.add_repository"), fontWeight = FontWeight.Bold, fontSize = 14.sp)
}
}
}
@Composable
private fun PluginRepositoryGroup(
repository: PluginRepositoryUiModel,
scrapers: List<PluginScraperUiModel>,
language: String?,
isRefreshing: Boolean,
onRemove: () -> Unit,
onRefresh: () -> Unit,
onToggleScraper: (String, Boolean) -> Unit,
onOpenScraperSettings: (PluginScraperUiModel) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(Color.White.copy(alpha = 0.04f))
.border(1.dp, Color.White.copy(alpha = 0.06f), RoundedCornerShape(18.dp))
.padding(vertical = 4.dp)
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = repository.name.ifBlank { repository.manifestUrl },
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = AppStrings.format(language, "settings.plugins.scraper_count", repository.scraperCount),
color = Color.White.copy(alpha = 0.5f),
fontSize = 12.sp
)
}
if (isRefreshing) {
Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator(color = Color.White.copy(alpha = 0.68f), strokeWidth = 2.dp, modifier = Modifier.size(18.dp))
}
} else {
PluginIconButton(icon = Icons.Filled.Refresh, onClick = onRefresh)
}
PluginIconButton(icon = Icons.Filled.Close, tint = Color.White.copy(alpha = 0.5f), onClick = onRemove)
}
if (scrapers.isNotEmpty()) {
HorizontalDivider(color = Color.White.copy(alpha = 0.06f), thickness = 0.5.dp)
scrapers.forEachIndexed { index, scraper ->
PluginScraperRow(
scraper = scraper,
language = language,
onToggle = { enabled -> onToggleScraper(scraper.id, enabled) },
onOpenSettings = { onOpenScraperSettings(scraper) }
)
if (index < scrapers.lastIndex) {
HorizontalDivider(color = Color.White.copy(alpha = 0.06f), thickness = 0.5.dp, modifier = Modifier.padding(horizontal = 16.dp))
}
}
}
}
}
@ -244,31 +332,31 @@ private fun PluginScraperRow(
onOpenSettings: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(scraper.name, color = Color.White)
Text(scraper.supportedTypes.joinToString(", "), color = Color.White.copy(alpha = 0.4f), fontSize = 11.sp)
Text(
text = scraper.name,
color = Color.White,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = scraper.supportedTypes.joinToString(", "),
color = Color.White.copy(alpha = 0.45f),
fontSize = 11.sp
)
}
if (scraper.hasSettings) {
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.05f))
.clickable(onClick = onOpenSettings),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Rounded.Settings,
contentDescription = AppStrings.t(language, "settings.plugins.settings"),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier.size(18.dp)
)
}
Spacer(Modifier.width(8.dp))
PluginIconButton(
icon = Icons.Filled.Settings,
contentDescription = AppStrings.t(language, "settings.plugins.settings"),
onClick = onOpenSettings
)
Spacer(Modifier.width(4.dp))
}
Switch(
checked = scraper.enabled,
@ -276,47 +364,35 @@ private fun PluginScraperRow(
colors = SwitchDefaults.colors(
checkedTrackColor = Color.White,
checkedThumbColor = Color.Black,
uncheckedTrackColor = Color.White.copy(alpha = 0.15f),
uncheckedThumbColor = Color.White.copy(alpha = 0.7f)
uncheckedTrackColor = Color.White.copy(alpha = 0.18f),
uncheckedThumbColor = Color.White.copy(alpha = 0.72f)
)
)
}
}
@Composable
private fun PluginAddRepositoryDialog(
language: String?,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit
private fun PluginIconButton(
icon: ImageVector,
contentDescription: String? = null,
tint: Color = Color.White.copy(alpha = 0.68f),
onClick: () -> Unit
) {
var url by remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(AppStrings.t(language, "settings.plugins.add_repository")) },
text = {
TextField(
value = url,
onValueChange = { url = it },
placeholder = { Text(AppStrings.t(language, "settings.plugins.repository_url_placeholder")) },
label = { Text(AppStrings.t(language, "settings.plugins.repository_url")) },
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White.copy(alpha = 0.05f),
unfocusedContainerColor = Color.White.copy(alpha = 0.05f),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White
)
)
},
confirmButton = {
TextButton(onClick = { onConfirm(url) }, enabled = url.isNotBlank()) {
Text(AppStrings.t(language, "settings.plugins.add"))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(AppStrings.t(language, "settings.plugins.cancel"))
}
}
)
var focused by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.onFocusChanged { focused = it.isFocused }
.background(if (focused) Color.White else Color.Transparent)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = contentDescription,
tint = if (focused) Color.Black else tint,
modifier = Modifier.size(18.dp)
)
}
}

View file

@ -43,6 +43,10 @@ class PluginsSettingsViewModel @Inject constructor(
viewModelScope.launch { manager.removeRepository(manifestUrl) }
}
fun refreshRepository(manifestUrl: String) {
viewModelScope.launch { manager.refreshRepository(manifestUrl) }
}
fun toggleScraper(scraperId: String, enabled: Boolean) {
viewModelScope.launch { manager.toggleScraper(scraperId, enabled) }
}

View file

@ -282,9 +282,8 @@
"settings.torrent_speed_default": "Default",
"settings.torrent_speed_fast": "Fast",
"settings.torrent_speed_ultra_fast": "Ultra Fast",
"settings.plugins.manage": "Scraper Plugins",
"settings.plugins.title": "Scraper Plugins",
"settings.plugins.repositories": "Repositories",
"settings.plugins.manage": "Plugins",
"settings.plugins.title": "Plugins",
"settings.plugins.add_repository": "Add Repository",
"settings.plugins.repository_url": "Repository manifest URL",
"settings.plugins.repository_url_placeholder": "https://example.com/repo.json",
@ -292,11 +291,8 @@
"settings.plugins.cancel": "Cancel",
"settings.plugins.remove": "Remove",
"settings.plugins.refresh": "Refresh",
"settings.plugins.scrapers": "Scrapers",
"settings.plugins.no_repositories": "No repositories added yet.",
"settings.plugins.no_scrapers": "No scrapers available.",
"settings.plugins.scraper_count": "%s scrapers",
"settings.plugins.adding": "Adding repository…",
"settings.plugins.error": "Failed to add repository: %s",
"settings.plugins.settings": "Settings",
"settings.plugins.settings_title": "%s Settings",
@ -1128,5 +1124,7 @@
"settings.bottom_bar_labels_desc": "Display destination names below navigation icons.",
"settings.home_top_bar": "Home top bar",
"settings.home_top_bar_desc": "Show Home, Series, Movies, notifications, and profile above the hero.",
"common.add_to_library": "Add to Library"
"common.add_to_library": "Add to Library",
"settings.plugins.installed_repositories": "Plugin Repositories",
"settings.plugins.adding": "Adding repository…"
}

View file

@ -282,9 +282,8 @@
"settings.torrent_speed_default": "Varsayılan",
"settings.torrent_speed_fast": "Hızlı",
"settings.torrent_speed_ultra_fast": "Ultra Hızlı",
"settings.plugins.manage": "Kazıyıcı Eklentiler",
"settings.plugins.title": "Kazıyıcı Eklentiler",
"settings.plugins.repositories": "Depolar",
"settings.plugins.manage": "Eklentiler",
"settings.plugins.title": "Eklentiler",
"settings.plugins.add_repository": "Depo Ekle",
"settings.plugins.repository_url": "Depo manifest URL'si",
"settings.plugins.repository_url_placeholder": "https://example.com/repo.json",
@ -292,11 +291,8 @@
"settings.plugins.cancel": "İptal",
"settings.plugins.remove": "Kaldır",
"settings.plugins.refresh": "Yenile",
"settings.plugins.scrapers": "Kazıyıcılar",
"settings.plugins.no_repositories": "Henüz depo eklenmedi.",
"settings.plugins.no_scrapers": "Kullanılabilir kazıyıcı yok.",
"settings.plugins.scraper_count": "%s kazıyıcı",
"settings.plugins.adding": "Depo ekleniyor…",
"settings.plugins.error": "Depo eklenemedi: %s",
"settings.plugins.settings": "Ayarlar",
"settings.plugins.settings_title": "%s Ayarları",
@ -1128,5 +1124,7 @@
"settings.bottom_bar_labels_desc": "Gezinme simgelerinin altında hedef adlarını gösterir.",
"settings.home_top_bar": "Ana sayfa üst çubuğu",
"settings.home_top_bar_desc": "Hero alanının üzerinde Ana Sayfa, Diziler, Filmler, bildirimler ve profili gösterir.",
"common.add_to_library": "Kitaplığa ekle"
"common.add_to_library": "Kitaplığa ekle",
"settings.plugins.installed_repositories": "Eklenti Depoları",
"settings.plugins.adding": "Depo ekleniyor…"
}