i18n(fr): refonte complète des traductions françaises

- Ajout des 38 clés FR manquantes (Trakt, TMDB Person/Director, IntroDB)
- Extraction des chaînes hardcodées vers stringResource :
  PluginsSettingsScreen (37), SubmitIntroDialog (10), PlaybackSettingsPage (1)
- Tutoiement systématique (~80 verbes -ez → -e/-s/-is, vos→tes, votre→ton/ta avec
  détection de genre, Vous pouvez/devez/...→Tu peux/dois/...)
- Typographie : apostrophes courbes ’, espaces insécables avant : ; ? !,
  guillemets français « » avec NBSP intérieurs
- Corrections d'accord (addon masculin partout, clé féminin, addons principaux)
- Cohérence terminologique (streams partout, chaînes pour network,
  liste de suivi pour watchlist, j'aime pour likes, enregistrée pour sauvegardée)
- Reformulations naturelles (Plus comme ceci → À voir aussi ×4, etc.)

Parité finale : 1243 clés EN ↔ FR.
This commit is contained in:
Stéphane 2026-05-05 13:25:44 +02:00
parent f7a7e37769
commit 105f4f52cb
5 changed files with 700 additions and 297 deletions

View file

@ -165,18 +165,27 @@
<string name="collections_editor_tmdb_quick_networks">Quick networks</string>
<string name="collections_editor_tmdb_genres">Genre IDs</string>
<string name="collections_editor_tmdb_genres_helper">Use TMDB genre numbers. Separate multiple with commas for AND, or pipes for OR.</string>
<string name="collections_editor_tmdb_genres_movie_placeholder">28,12</string>
<string name="collections_editor_tmdb_genres_series_placeholder">18,35</string>
<string name="collections_editor_tmdb_date_from">Release or air date from</string>
<string name="collections_editor_tmdb_date_to">Release or air date to</string>
<string name="collections_editor_tmdb_date_helper">Use YYYY-MM-DD, for example 2024-01-01.</string>
<string name="collections_editor_tmdb_date_from_placeholder">2020-01-01</string>
<string name="collections_editor_tmdb_date_to_placeholder">2024-12-31</string>
<string name="collections_editor_tmdb_rating_min">Minimum rating</string>
<string name="collections_editor_tmdb_rating_max">Maximum rating</string>
<string name="collections_editor_tmdb_rating_helper">TMDB rating from 0 to 10. Example: 7.0.</string>
<string name="collections_editor_tmdb_rating_min_placeholder">7.0</string>
<string name="collections_editor_tmdb_rating_max_placeholder">10</string>
<string name="collections_editor_tmdb_votes_min">Minimum votes</string>
<string name="collections_editor_tmdb_votes_helper">Use this to avoid obscure low-vote titles. Example: 100.</string>
<string name="collections_editor_tmdb_votes_min_placeholder">100</string>
<string name="collections_editor_tmdb_language">Original language</string>
<string name="collections_editor_tmdb_language_helper">Use two-letter language codes, for example en, ko, ja, hi.</string>
<string name="collections_editor_tmdb_language_placeholder">en, ko, ja, hi</string>
<string name="collections_editor_tmdb_country">Origin country</string>
<string name="collections_editor_tmdb_country_helper">Use two-letter country codes, for example US, KR, JP, IN.</string>
<string name="collections_editor_tmdb_country_placeholder">US, KR, JP, IN</string>
<string name="collections_editor_tmdb_keywords">Keyword IDs</string>
<string name="collections_editor_tmdb_keywords_helper">Use TMDB keyword numbers. Quick chips fill common examples.</string>
<string name="collections_editor_tmdb_keywords_placeholder">9715 for superhero</string>
@ -188,6 +197,7 @@
<string name="collections_editor_tmdb_networks_placeholder">213 for Netflix</string>
<string name="collections_editor_tmdb_year">Year</string>
<string name="collections_editor_tmdb_year_helper">Use a four-digit year, for example 2024.</string>
<string name="collections_editor_tmdb_year_placeholder">2024</string>
<string name="collections_editor_tmdb_presets">Presets</string>
<string name="collections_editor_tmdb_search">Search</string>
<string name="collections_editor_add_source">Add Source</string>
@ -212,6 +222,12 @@
<string name="collections_editor_trakt_sort_popular">Popular</string>
<string name="collections_editor_trakt_sort_percentage">Percentage</string>
<string name="collections_editor_trakt_sort_votes">Votes</string>
<string name="collections_editor_trakt_enter_name_url_or_id">Enter a Trakt list name, URL, or ID</string>
<string name="collections_editor_trakt_enter_id_or_url">Enter a Trakt list ID or URL</string>
<string name="collections_editor_trakt_load_failed">Could not load Trakt list</string>
<string name="collections_editor_trakt_no_lists_found">No Trakt lists found</string>
<string name="collections_editor_trakt_resolved_subtitle">Resolved Trakt list</string>
<string name="collections_editor_trakt_fallback_title">Trakt List %1$d</string>
<string name="collections_editor_tmdb_genre_action">Action</string>
<string name="collections_editor_tmdb_genre_adventure">Adventure</string>
<string name="collections_editor_tmdb_genre_animation">Animation</string>
@ -1072,6 +1088,8 @@
<string name="meta_section_trailers_description">Trailer rail and playback shortcuts.</string>
<string name="network_back_online">Back online</string>
<string name="network_cannot_reach_servers">Cannot reach servers</string>
<string name="network_error_empty_response_body">Empty response body</string>
<string name="network_error_request_failed_http">Request failed with HTTP %1$d</string>
<string name="network_no_internet_connection">No internet connection</string>
<string name="person_age">(age %1$d)</string>
<string name="person_born">Born %1$s%2$s</string>
@ -1165,6 +1183,8 @@
<string name="updates_asset_line">%1$s • %2$s</string>
<string name="updates_check_failed">Update check failed</string>
<string name="updates_download_failed">Download failed</string>
<string name="updates_download_empty_body">Empty download body</string>
<string name="updates_download_file_missing">Downloaded update file is missing.</string>
<string name="updates_downloading_progress">Downloading %1$d%</string>
<string name="updates_install_failed">Unable to start installation</string>
<string name="updates_latest_version">You&apos;re using the latest version.</string>
@ -1212,6 +1232,7 @@
<string name="notifications_test_send_failed">Failed to send a test notification.</string>
<string name="notifications_test_sent_for">Test notification sent for %1$s.</string>
<string name="player_unable_to_play_stream">Unable to play this stream.</string>
<string name="player_engine_unavailable_rebuild">MPV player engine not available. Please rebuild the app.</string>
<string name="profile_pin_changed_requires_refresh">This profile PIN changed. Connect once to refresh the lock on this device.</string>
<string name="profile_pin_clear_failed">Couldn't remove PIN lock. Try again.</string>
<string name="profile_pin_clear_requires_internet">Connect to the internet to remove the PIN lock.</string>
@ -1224,6 +1245,8 @@
<string name="source_embedded">Embedded</string>
<string name="trakt_authorization_denied">Authorization denied</string>
<string name="trakt_complete_sign_in_browser">Complete Trakt sign in in your browser</string>
<string name="trakt_connected">Connected to Trakt</string>
<string name="trakt_disconnected">Disconnected from Trakt</string>
<string name="trakt_invalid_callback">Invalid Trakt callback</string>
<string name="trakt_invalid_callback_state">Invalid Trakt callback state</string>
<string name="trakt_invalid_token_response">Invalid Trakt token response</string>
@ -1232,9 +1255,39 @@
<string name="trakt_missing_auth_code">Trakt did not return an authorization code</string>
<string name="trakt_missing_credentials">Missing Trakt credentials</string>
<string name="trakt_progress_load_failed">Failed to load Trakt progress</string>
<string name="trakt_public_list">Trakt public list</string>
<string name="trakt_public_list_enter_valid_id_or_url">Enter a valid Trakt list ID or URL</string>
<string name="trakt_public_list_items_count">%1$d items</string>
<string name="trakt_public_list_likes_count">%1$d likes</string>
<string name="trakt_public_list_missing_credentials">Missing Trakt credentials in local.properties (TRAKT_CLIENT_ID).</string>
<string name="trakt_public_list_missing_id">Missing Trakt list ID</string>
<string name="trakt_public_list_missing_numeric_id">Trakt list did not include a numeric ID</string>
<string name="trakt_public_list_not_found_or_not_public">Trakt list not found or not public</string>
<string name="trakt_public_list_rate_limit_reached">Trakt rate limit reached</string>
<string name="trakt_public_list_request_failed">Trakt request failed</string>
<string name="trakt_sign_in_complete_failed">Failed to complete Trakt sign in</string>
<string name="trakt_user_fallback">Trakt user</string>
<string name="trakt_watchlist">Watchlist</string>
<string name="tmdb_sources_api_key_required">Add a TMDB API key in Settings to use TMDB sources.</string>
<string name="tmdb_sources_collection_fallback_title">TMDB Collection %1$d</string>
<string name="tmdb_sources_collection_not_found">TMDB collection not found</string>
<string name="tmdb_sources_company_fallback_title">TMDB Production %1$d</string>
<string name="tmdb_sources_company_not_found">TMDB company not found</string>
<string name="tmdb_sources_director_fallback_title">TMDB Director %1$d</string>
<string name="tmdb_sources_discover_no_data">TMDB discover returned no data</string>
<string name="tmdb_sources_discover_title">TMDB Discover</string>
<string name="tmdb_sources_invalid_id_or_url">Enter a valid TMDB ID or URL.</string>
<string name="tmdb_sources_list_fallback_title">TMDB List %1$d</string>
<string name="tmdb_sources_list_not_found">TMDB list not found</string>
<string name="tmdb_sources_load_failed">Could not load TMDB source</string>
<string name="tmdb_sources_missing_collection_id">Missing TMDB collection ID</string>
<string name="tmdb_sources_missing_list_id">Missing TMDB list ID</string>
<string name="tmdb_sources_missing_person_id">Missing TMDB person ID</string>
<string name="tmdb_sources_network_fallback_title">TMDB Network %1$d</string>
<string name="tmdb_sources_network_not_found">TMDB network not found</string>
<string name="tmdb_sources_person_credits_not_found">TMDB person credits not found</string>
<string name="tmdb_sources_person_fallback_title">TMDB Person %1$d</string>
<string name="tmdb_sources_person_not_found">TMDB person not found</string>
<string name="generic_trailer">Trailer</string>
<string name="generic_unknown">Unknown</string>
<string name="generic_addon">Addon</string>
@ -1250,6 +1303,8 @@
<string name="collections_import_error_trakt_list_id">Source %1$d in folder '%2$s' is missing a Trakt list ID.</string>
<string name="collections_import_error_invalid_json">Invalid JSON: %1$s</string>
<string name="collections_folder_addon_not_found">Addon not found: %1$s</string>
<string name="collections_folder_trakt_movie_list">Trakt Movie List</string>
<string name="collections_folder_trakt_series_list">Trakt Series List</string>
<string name="date_month_january">January</string>
<string name="date_month_february">February</string>
<string name="date_month_march">March</string>
@ -1298,9 +1353,13 @@
<string name="downloads_enqueue_started">Download started</string>
<string name="downloads_enqueue_unsupported_format">Unsupported stream format for downloads</string>
<string name="downloads_error_empty_body">Empty response body</string>
<string name="downloads_error_finalize_failed">Failed to finalize download file</string>
<string name="downloads_error_http_failed">Request failed with HTTP %1$d</string>
<string name="downloads_error_not_initialized">Download system is not initialized</string>
<string name="downloads_error_open_partial_failed">Failed to open partial download file</string>
<string name="downloads_error_partial_not_open">Partial download file is not open</string>
<string name="downloads_error_request_failed">Download request failed</string>
<string name="downloads_error_write_partial_failed">Failed to write partial download file</string>
<string name="home_catalog_default_title">%1$s - %2$s</string>
<string name="library_empty_message">Saved titles will appear here after you tap Save on a details screen.</string>
<string name="library_empty_title">Your library is empty</string>
@ -1339,4 +1398,60 @@
<string name="unit_bytes_kb">KB</string>
<string name="unit_bytes_mb">MB</string>
<string name="unit_bytes_gb">GB</string>
<string name="plugins_section_overview">OVERVIEW</string>
<string name="plugins_badge_repos">%1$d repos</string>
<string name="plugins_badge_providers">%1$d providers</string>
<string name="plugins_badge_enabled">Plugins enabled</string>
<string name="plugins_badge_disabled">Plugins disabled</string>
<string name="plugins_badge_tmdb_key_set">TMDB API key set</string>
<string name="plugins_badge_tmdb_key_missing">TMDB API key missing</string>
<string name="plugins_tmdb_required_message">Plugin providers require a TMDB API key. Set it on the TMDB screen or plugin providers will not work correctly.</string>
<string name="plugins_enable_globally_title">Enable plugin providers globally</string>
<string name="plugins_enable_globally_desc">Use plugin providers during stream discovery.</string>
<string name="plugins_group_by_repo_title">Group plugin providers by repository</string>
<string name="plugins_group_by_repo_desc">In Streams, show one provider per repository instead of one per source.</string>
<string name="plugins_section_add_repo">ADD REPOSITORY</string>
<string name="plugins_input_manifest_placeholder">Plugin manifest URL</string>
<string name="plugins_button_installing">Installing…</string>
<string name="plugins_button_install_repo">Install Plugin Repository</string>
<string name="plugins_error_enter_repo_url">Enter a plugin repository URL.</string>
<string name="plugins_error_enter_valid_url">Enter a valid plugin URL.</string>
<string name="plugins_error_already_installed">That plugin repository is already installed.</string>
<string name="plugins_error_install_failed">Unable to install plugin repository</string>
<string name="plugins_error_refresh_failed">Unable to refresh repository</string>
<string name="plugins_error_provider_not_found">Provider not found</string>
<string name="plugins_error_unavailable_build">Plugins are not available in this build.</string>
<string name="plugins_manifest_error_name_missing">Manifest name is missing.</string>
<string name="plugins_manifest_error_version_missing">Manifest version is missing.</string>
<string name="plugins_manifest_error_no_providers">Manifest has no providers.</string>
<string name="plugins_message_installed">Installed %1$s.</string>
<string name="plugins_section_installed_repos">INSTALLED REPOSITORIES</string>
<string name="plugins_empty_repos_title">No plugin repositories installed yet.</string>
<string name="plugins_empty_repos_subtitle">Add a repository URL to install provider plugins for stream discovery.</string>
<string name="plugins_repo_version">Version %1$s</string>
<string name="plugins_cd_refresh_repo">Refresh plugin repository</string>
<string name="plugins_cd_delete_repo">Delete plugin repository</string>
<string name="plugins_badge_refreshing">Refreshing</string>
<string name="plugins_section_providers">PROVIDERS</string>
<string name="plugins_empty_providers">No providers available yet.</string>
<string name="plugins_provider_no_description">No description</string>
<string name="plugins_provider_version">v%1$s</string>
<string name="plugins_provider_disabled_by_repo">Disabled by repo</string>
<string name="plugins_button_testing">Testing…</string>
<string name="plugins_button_test_provider">Test Provider</string>
<string name="plugins_test_error_title">Error</string>
<string name="plugins_test_failed">Provider test failed</string>
<string name="plugins_test_results_count">Test results (%1$d)</string>
<string name="plugins_repo_fallback_label">Plugin repository</string>
<string name="submit_intro_action">Submit Intro</string>
<string name="submit_intro_title">Submit Timestamps</string>
<string name="submit_intro_segment_type_label">SEGMENT TYPE</string>
<string name="submit_intro_segment_intro">Intro</string>
<string name="submit_intro_segment_recap">Recap</string>
<string name="submit_intro_segment_outro">Outro</string>
<string name="submit_intro_start_time_label">START TIME (MM:SS)</string>
<string name="submit_intro_end_time_label">END TIME (MM:SS)</string>
<string name="submit_intro_button_submit">Submit</string>
<string name="submit_intro_capture_button">Capture</string>
<string name="settings_playback_introdb_invalid_key">Invalid API Key or connection failed</string>
</resources>

View file

@ -49,6 +49,19 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.action_cancel
import nuvio.composeapp.generated.resources.action_close
import nuvio.composeapp.generated.resources.submit_intro_button_submit
import nuvio.composeapp.generated.resources.submit_intro_capture_button
import nuvio.composeapp.generated.resources.submit_intro_end_time_label
import nuvio.composeapp.generated.resources.submit_intro_segment_intro
import nuvio.composeapp.generated.resources.submit_intro_segment_outro
import nuvio.composeapp.generated.resources.submit_intro_segment_recap
import nuvio.composeapp.generated.resources.submit_intro_segment_type_label
import nuvio.composeapp.generated.resources.submit_intro_start_time_label
import nuvio.composeapp.generated.resources.submit_intro_title
import org.jetbrains.compose.resources.stringResource
import kotlin.math.floor
@OptIn(ExperimentalMaterial3Api::class)
@ -91,20 +104,24 @@ fun SubmitIntroDialog(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Submit Timestamps",
text = stringResource(Res.string.submit_intro_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Bold,
)
IconButton(onClick = onDismiss) {
Icon(Icons.Rounded.Close, contentDescription = "Close", tint = MaterialTheme.colorScheme.onSurfaceVariant)
Icon(
Icons.Rounded.Close,
contentDescription = stringResource(Res.string.action_close),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
// Segment Type
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "SEGMENT TYPE",
text = stringResource(Res.string.submit_intro_segment_type_label),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold,
@ -114,21 +131,21 @@ fun SubmitIntroDialog(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
SegmentTypeButton(
label = "Intro",
label = stringResource(Res.string.submit_intro_segment_intro),
icon = Icons.Rounded.PlayCircleOutline,
selected = segmentType == "intro",
onClick = { onSegmentTypeChange("intro") },
modifier = Modifier.weight(1f)
)
SegmentTypeButton(
label = "Recap",
label = stringResource(Res.string.submit_intro_segment_recap),
icon = Icons.Rounded.Replay,
selected = segmentType == "recap",
onClick = { onSegmentTypeChange("recap") },
modifier = Modifier.weight(1f)
)
SegmentTypeButton(
label = "Outro",
label = stringResource(Res.string.submit_intro_segment_outro),
icon = Icons.Rounded.StopCircle,
selected = segmentType == "outro",
onClick = { onSegmentTypeChange("outro") },
@ -139,7 +156,7 @@ fun SubmitIntroDialog(
// Start Time
TimeInputRow(
label = "START TIME (MM:SS)",
label = stringResource(Res.string.submit_intro_start_time_label),
value = startTimeStr,
onValueChange = onStartTimeChange,
onCapture = { onStartTimeChange(formatSecondsToMMSS(currentTimeSec)) }
@ -147,7 +164,7 @@ fun SubmitIntroDialog(
// End Time
TimeInputRow(
label = "END TIME (MM:SS)",
label = stringResource(Res.string.submit_intro_end_time_label),
value = endTimeStr,
onValueChange = onEndTimeChange,
onCapture = { onEndTimeChange(formatSecondsToMMSS(currentTimeSec)) }
@ -170,7 +187,7 @@ fun SubmitIntroDialog(
contentAlignment = Alignment.Center
) {
Text(
text = "Cancel",
text = stringResource(Res.string.action_cancel),
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.SemiBold
)
@ -217,7 +234,7 @@ fun SubmitIntroDialog(
) {
Icon(Icons.Rounded.Send, contentDescription = null, tint = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.size(18.dp))
Text(
text = "Submit",
text = stringResource(Res.string.submit_intro_button_submit),
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Bold
)
@ -328,7 +345,7 @@ private fun TimeInputRow(
modifier = Modifier.size(18.dp)
)
Text(
text = "Capture",
text = stringResource(Res.string.submit_intro_capture_button),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.SemiBold

View file

@ -2293,6 +2293,7 @@ private fun IntroDbApiKeyDialog(
var value by remember { mutableStateOf(initialValue) }
var isVerifying by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val invalidKeyMessage = stringResource(Res.string.settings_playback_introdb_invalid_key)
BasicAlertDialog(onDismissRequest = { if (!isVerifying) onDismiss() }) {
Surface(
@ -2361,7 +2362,7 @@ private fun IntroDbApiKeyDialog(
if (isValid) {
onSave(trimmed)
} else {
errorMessage = "Invalid API Key or connection failed"
errorMessage = invalidKeyMessage
}
}
},

View file

@ -40,6 +40,44 @@ import com.nuvio.app.core.ui.NuvioSectionLabel
import com.nuvio.app.core.ui.NuvioSurfaceCard
import com.nuvio.app.features.tmdb.TmdbSettingsRepository
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.Res
import nuvio.composeapp.generated.resources.plugins_badge_disabled
import nuvio.composeapp.generated.resources.plugins_badge_enabled
import nuvio.composeapp.generated.resources.plugins_badge_providers
import nuvio.composeapp.generated.resources.plugins_badge_refreshing
import nuvio.composeapp.generated.resources.plugins_badge_repos
import nuvio.composeapp.generated.resources.plugins_badge_tmdb_key_missing
import nuvio.composeapp.generated.resources.plugins_badge_tmdb_key_set
import nuvio.composeapp.generated.resources.plugins_button_install_repo
import nuvio.composeapp.generated.resources.plugins_button_installing
import nuvio.composeapp.generated.resources.plugins_button_test_provider
import nuvio.composeapp.generated.resources.plugins_button_testing
import nuvio.composeapp.generated.resources.plugins_cd_delete_repo
import nuvio.composeapp.generated.resources.plugins_cd_refresh_repo
import nuvio.composeapp.generated.resources.plugins_empty_providers
import nuvio.composeapp.generated.resources.plugins_empty_repos_subtitle
import nuvio.composeapp.generated.resources.plugins_empty_repos_title
import nuvio.composeapp.generated.resources.plugins_enable_globally_desc
import nuvio.composeapp.generated.resources.plugins_enable_globally_title
import nuvio.composeapp.generated.resources.plugins_error_enter_repo_url
import nuvio.composeapp.generated.resources.plugins_group_by_repo_desc
import nuvio.composeapp.generated.resources.plugins_group_by_repo_title
import nuvio.composeapp.generated.resources.plugins_input_manifest_placeholder
import nuvio.composeapp.generated.resources.plugins_message_installed
import nuvio.composeapp.generated.resources.plugins_provider_disabled_by_repo
import nuvio.composeapp.generated.resources.plugins_provider_no_description
import nuvio.composeapp.generated.resources.plugins_provider_version
import nuvio.composeapp.generated.resources.plugins_repo_fallback_label
import nuvio.composeapp.generated.resources.plugins_repo_version
import nuvio.composeapp.generated.resources.plugins_section_add_repo
import nuvio.composeapp.generated.resources.plugins_section_installed_repos
import nuvio.composeapp.generated.resources.plugins_section_overview
import nuvio.composeapp.generated.resources.plugins_section_providers
import nuvio.composeapp.generated.resources.plugins_test_error_title
import nuvio.composeapp.generated.resources.plugins_test_failed
import nuvio.composeapp.generated.resources.plugins_test_results_count
import nuvio.composeapp.generated.resources.plugins_tmdb_required_message
import org.jetbrains.compose.resources.stringResource
@Composable
fun PluginsSettingsPageContent(
@ -79,29 +117,43 @@ fun PluginsSettingsPageContent(
)
}
val repoFallbackLabel = stringResource(Res.string.plugins_repo_fallback_label)
val testFailedDefault = stringResource(Res.string.plugins_test_failed)
val testErrorTitle = stringResource(Res.string.plugins_test_error_title)
val installedTemplate = stringResource(Res.string.plugins_message_installed)
val enterRepoUrlError = stringResource(Res.string.plugins_error_enter_repo_url)
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
NuvioSectionLabel("OVERVIEW")
NuvioSectionLabel(stringResource(Res.string.plugins_section_overview))
NuvioSurfaceCard {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
NuvioInfoBadge(text = "${sortedRepos.size} repos")
NuvioInfoBadge(text = "${sortedScrapers.size} providers")
NuvioInfoBadge(text = stringResource(Res.string.plugins_badge_repos, sortedRepos.size))
NuvioInfoBadge(text = stringResource(Res.string.plugins_badge_providers, sortedScrapers.size))
NuvioInfoBadge(
text = if (uiState.pluginsEnabled) "Plugins enabled" else "Plugins disabled",
text = if (uiState.pluginsEnabled) {
stringResource(Res.string.plugins_badge_enabled)
} else {
stringResource(Res.string.plugins_badge_disabled)
},
)
NuvioInfoBadge(
text = if (hasTmdbApiKey) "TMDB API key set" else "TMDB API key missing",
text = if (hasTmdbApiKey) {
stringResource(Res.string.plugins_badge_tmdb_key_set)
} else {
stringResource(Res.string.plugins_badge_tmdb_key_missing)
},
)
}
if (!hasTmdbApiKey) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Plugin providers require a TMDB API key. Set it on the TMDB screen or plugin providers will not work correctly.",
text = stringResource(Res.string.plugins_tmdb_required_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
)
@ -114,13 +166,13 @@ fun PluginsSettingsPageContent(
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Enable plugin providers globally",
text = stringResource(Res.string.plugins_enable_globally_title),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "Use plugin providers during stream discovery.",
text = stringResource(Res.string.plugins_enable_globally_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -143,13 +195,13 @@ fun PluginsSettingsPageContent(
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Group plugin providers by repository",
text = stringResource(Res.string.plugins_group_by_repo_title),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = "In Streams, show one provider per repository instead of one per source.",
text = stringResource(Res.string.plugins_group_by_repo_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -162,7 +214,7 @@ fun PluginsSettingsPageContent(
}
}
NuvioSectionLabel("ADD REPOSITORY")
NuvioSectionLabel(stringResource(Res.string.plugins_section_add_repo))
NuvioSurfaceCard {
NuvioInputField(
value = repositoryUrl,
@ -170,16 +222,20 @@ fun PluginsSettingsPageContent(
repositoryUrl = it
message = null
},
placeholder = "Plugin manifest URL",
placeholder = stringResource(Res.string.plugins_input_manifest_placeholder),
)
Spacer(modifier = Modifier.height(16.dp))
NuvioPrimaryButton(
text = if (isAdding) "Installing..." else "Install Plugin Repository",
text = if (isAdding) {
stringResource(Res.string.plugins_button_installing)
} else {
stringResource(Res.string.plugins_button_install_repo)
},
enabled = repositoryUrl.isNotBlank() && !isAdding,
onClick = {
val requested = repositoryUrl.trim()
if (requested.isBlank()) {
message = "Enter a plugin repository URL."
message = enterRepoUrlError
return@NuvioPrimaryButton
}
isAdding = true
@ -188,7 +244,7 @@ fun PluginsSettingsPageContent(
when (val result = PluginRepository.addRepository(requested)) {
is AddPluginRepositoryResult.Success -> {
repositoryUrl = ""
message = "Installed ${result.repository.name}."
message = installedTemplate.format(result.repository.name)
}
is AddPluginRepositoryResult.Error -> {
message = result.message
@ -208,17 +264,17 @@ fun PluginsSettingsPageContent(
}
}
NuvioSectionLabel("INSTALLED REPOSITORIES")
NuvioSectionLabel(stringResource(Res.string.plugins_section_installed_repos))
if (sortedRepos.isEmpty()) {
NuvioSurfaceCard {
Text(
text = "No plugin repositories installed yet.",
text = stringResource(Res.string.plugins_empty_repos_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Add a repository URL to install provider plugins for stream discovery.",
text = stringResource(Res.string.plugins_empty_repos_subtitle),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -242,7 +298,7 @@ fun PluginsSettingsPageContent(
repo.version?.let { version ->
Spacer(modifier = Modifier.height(6.dp))
Text(
text = "Version $version",
text = stringResource(Res.string.plugins_repo_version, version),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -259,13 +315,13 @@ fun PluginsSettingsPageContent(
Row(verticalAlignment = Alignment.CenterVertically) {
NuvioIconActionButton(
icon = Icons.Rounded.Refresh,
contentDescription = "Refresh plugin repository",
contentDescription = stringResource(Res.string.plugins_cd_refresh_repo),
tint = MaterialTheme.colorScheme.primary,
onClick = { PluginRepository.refreshRepository(repo.manifestUrl, pushAfterRefresh = true) },
)
NuvioIconActionButton(
icon = Icons.Rounded.Delete,
contentDescription = "Delete plugin repository",
contentDescription = stringResource(Res.string.plugins_cd_delete_repo),
tint = MaterialTheme.colorScheme.error,
onClick = { PluginRepository.removeRepository(repo.manifestUrl) },
)
@ -276,9 +332,9 @@ fun PluginsSettingsPageContent(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
NuvioInfoBadge(text = "${repo.scraperCount} providers")
NuvioInfoBadge(text = stringResource(Res.string.plugins_badge_providers, repo.scraperCount))
if (repo.isRefreshing) {
NuvioInfoBadge(text = "Refreshing")
NuvioInfoBadge(text = stringResource(Res.string.plugins_badge_refreshing))
}
}
repo.errorMessage?.let { errorMessage ->
@ -293,11 +349,11 @@ fun PluginsSettingsPageContent(
}
}
NuvioSectionLabel("PROVIDERS")
NuvioSectionLabel(stringResource(Res.string.plugins_section_providers))
if (sortedScrapers.isEmpty()) {
NuvioSurfaceCard {
Text(
text = "No providers available yet.",
text = stringResource(Res.string.plugins_empty_providers),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@ -307,7 +363,7 @@ fun PluginsSettingsPageContent(
val scraperResults = testResults[scraper.id]
val isTestingThisScraper = testingScraperId == scraper.id
val repositoryName = repositoryNameByUrl[scraper.repositoryUrl]
?: scraper.repositoryUrl.fallbackRepositoryLabel()
?: scraper.repositoryUrl.fallbackRepositoryLabel(repoFallbackLabel)
NuvioSurfaceCard {
Row(
@ -342,7 +398,9 @@ fun PluginsSettingsPageContent(
overflow = TextOverflow.Ellipsis,
)
Text(
text = scraper.description.ifBlank { "No description" },
text = scraper.description.ifBlank {
stringResource(Res.string.plugins_provider_no_description)
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
@ -363,15 +421,19 @@ fun PluginsSettingsPageContent(
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
NuvioInfoBadge(text = scraper.supportedTypes.joinToString(" | "))
NuvioInfoBadge(text = "v${scraper.version}")
NuvioInfoBadge(text = stringResource(Res.string.plugins_provider_version, scraper.version))
if (!scraper.manifestEnabled) {
NuvioInfoBadge(text = "Disabled by repo")
NuvioInfoBadge(text = stringResource(Res.string.plugins_provider_disabled_by_repo))
}
}
Spacer(modifier = Modifier.height(12.dp))
NuvioPrimaryButton(
text = if (isTestingThisScraper) "Testing..." else "Test Provider",
text = if (isTestingThisScraper) {
stringResource(Res.string.plugins_button_testing)
} else {
stringResource(Res.string.plugins_button_test_provider)
},
enabled = hasTmdbApiKey && !isTestingThisScraper,
onClick = {
testingScraperId = scraper.id
@ -383,8 +445,8 @@ fun PluginsSettingsPageContent(
.onFailure { error ->
testResults[scraper.id] = listOf(
PluginRuntimeResult(
title = "Error",
name = error.message ?: "Provider test failed",
title = testErrorTitle,
name = error.message ?: testFailedDefault,
url = "about:error",
),
)
@ -399,7 +461,7 @@ fun PluginsSettingsPageContent(
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Test results (${scraperResults.size})",
text = stringResource(Res.string.plugins_test_results_count, scraperResults.size),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
@ -441,11 +503,11 @@ fun PluginsSettingsPageContent(
}
}
private fun String.fallbackRepositoryLabel(): String {
private fun String.fallbackRepositoryLabel(fallback: String): String {
val withoutQuery = substringBefore("?")
val withoutManifest = withoutQuery.removeSuffix("/manifest.json")
val host = withoutManifest.substringAfter("://", withoutManifest).substringBefore('/')
return host.ifBlank {
withoutManifest.substringAfterLast('/').ifBlank { "Plugin repository" }
withoutManifest.substringAfterLast('/').ifBlank { fallback }
}
}