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: Big Buck Bunny seeding with file list expanded showing Play button on the mp4, Sintel seeding, and Cosmos Laundromat downloading at 2.9 MB/s with 25 peers, ETA and ratio | +| Torrent list with file list expanded and active download | ExoPlayer with multi-language subtitles | +|---|---| +| Torrent list: Big Buck Bunny seeding with file list expanded showing Play button on the mp4, Sintel seeding, and Cosmos Laundromat downloading at 2.9 MB/s with 25 peers, ETA and ratio | Video playback in fullscreen with German subtitles displayed. Subtitle selector dropdown open on the right showing language options: None, German, English, Spanish, French, Italian, Dutch | ## 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