diff --git a/README.md b/README.md
index 2a0defd..3a7f81b 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Android NDK and the libtorrent-rasterbar C++ library.
- Collapsible per-file progress list — file name, size, and individual download progress bar
- ETA, total size, and upload ratio on each card (e.g. `151.7 MB / 263.6 MB • ETA 27s • ratio 0.42`)
- Sequential download + in-app ExoPlayer — tap Play (≥ 5 % downloaded) to watch before the file is complete; position is remembered across sessions and cleared when the torrent is deleted
+- Multi-language subtitle support — automatically detects `.srt` files and provides a language selector in the player UI
- Pause, resume, and delete torrents (with optional file removal)
- Persistent sessions — resume data is saved on `onStop` and restored on next launch
- Share `.torrent` files to other clients via the system share sheet
@@ -24,9 +25,9 @@ https://github.com/user-attachments/assets/6182efdb-9862-47ee-9fa7-011a64ee87eb
## Screenshots
-| Torrent list with file list expanded and active download |
-|---|
-|
|
+| Torrent list with file list expanded and active download | ExoPlayer with multi-language subtitles |
+|---|---|
+|
|
|
## Technical Stack
@@ -71,7 +72,7 @@ SimpleTorrent/
│ │ │ ├── MainScreen.kt # Compose UI: cards, piece map, file list, peer list
│ │ │ └── MainScreenViewModel.kt
│ │ └── player/
-│ │ └── PlayerScreen.kt # ExoPlayer fullscreen player; position saved per file path
+│ │ └── PlayerScreen.kt # ExoPlayer fullscreen player; position saved per file path; auto-detects and loads multi-language subtitles
│ └── res/
│ ├── drawable/ # Adaptive icon (vector magnet)
│ └── xml/file_provider_paths.xml
diff --git a/app/src/main/java/com/jpcottin/simpletorrent/ui/player/PlayerScreen.kt b/app/src/main/java/com/jpcottin/simpletorrent/ui/player/PlayerScreen.kt
index 9763aea..ddf92d7 100644
--- a/app/src/main/java/com/jpcottin/simpletorrent/ui/player/PlayerScreen.kt
+++ b/app/src/main/java/com/jpcottin/simpletorrent/ui/player/PlayerScreen.kt
@@ -11,7 +11,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.ClosedCaption
import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@@ -38,6 +41,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.jpcottin.simpletorrent.theme.SimpleTorrentTheme
import java.io.File
+import java.util.Locale
@androidx.annotation.OptIn(UnstableApi::class)
@Composable
@@ -47,11 +51,40 @@ fun PlayerScreen(filePath: String, title: String, onBack: () -> Unit) {
val isPreview = LocalInspectionMode.current
val prefs = remember { context.getSharedPreferences("player_positions", Context.MODE_PRIVATE) }
var isBuffering by remember { mutableStateOf(true) }
+ var showSubtitleMenu by remember { mutableStateOf(false) }
+ var subtitles by remember { mutableStateOf>>(emptyList()) }
val player = remember {
if (isPreview) return@remember null
+ val videoFile = File(filePath)
+ val videoDir = videoFile.parentFile
+ val baseNameNoExt = videoFile.nameWithoutExtension
+
+ // Find all .srt files in the same directory
+ val srtConfigs = videoDir?.listFiles { file ->
+ file.isFile && file.extension == "srt"
+ }?.mapNotNull { srtFile ->
+ // Extract language code from filename pattern: *_xx.srt or subtitle_xx.srt
+ val match = Regex("([a-z]{2})\\.srt$").find(srtFile.name)
+ if (match != null) {
+ val languageCode = match.groupValues[1]
+ val displayLanguage = Locale(languageCode).displayLanguage
+ subtitles = subtitles + (languageCode to displayLanguage)
+ MediaItem.SubtitleConfiguration.Builder(Uri.fromFile(srtFile))
+ .setMimeType("application/x-subrip")
+ .setLanguage(languageCode)
+ .setLabel(displayLanguage)
+ .build()
+ } else null
+ } ?: emptyList()
+
ExoPlayer.Builder(context).build().apply {
- setMediaItem(MediaItem.fromUri(Uri.fromFile(File(filePath))))
+ val mediaItem = MediaItem.Builder()
+ .setUri(Uri.fromFile(videoFile))
+ .setSubtitleConfigurations(srtConfigs)
+ .build()
+
+ setMediaItem(mediaItem)
prepare()
val savedPosition = prefs.getLong(filePath, 0L)
if (savedPosition > 0L) seekTo(savedPosition)
@@ -131,6 +164,46 @@ fun PlayerScreen(filePath: String, title: String, onBack: () -> Unit) {
tint = Color.White,
)
}
+ if (subtitles.isNotEmpty() && player != null) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .padding(WindowInsets.safeDrawing.asPaddingValues()),
+ ) {
+ IconButton(onClick = { showSubtitleMenu = !showSubtitleMenu }) {
+ Icon(
+ Icons.Default.ClosedCaption,
+ contentDescription = "Subtitles",
+ tint = Color.White,
+ )
+ }
+ DropdownMenu(
+ expanded = showSubtitleMenu,
+ onDismissRequest = { showSubtitleMenu = false },
+ ) {
+ DropdownMenuItem(
+ text = { Text("None") },
+ onClick = {
+ player.trackSelectionParameters = player.trackSelectionParameters.buildUpon()
+ .setPreferredTextLanguages()
+ .build()
+ showSubtitleMenu = false
+ },
+ )
+ subtitles.forEach { (languageCode, language) ->
+ DropdownMenuItem(
+ text = { Text(language) },
+ onClick = {
+ player.trackSelectionParameters = player.trackSelectionParameters.buildUpon()
+ .setPreferredTextLanguages(languageCode)
+ .build()
+ showSubtitleMenu = false
+ },
+ )
+ }
+ }
+ }
+ }
}
}
diff --git a/screenshots/player_subtitles.png b/screenshots/player_subtitles.png
new file mode 100644
index 0000000..ea9dfe1
Binary files /dev/null and b/screenshots/player_subtitles.png differ