Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
|---|
| <img src="screenshots/main.png" width="320" alt="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 |
|---|---|
| <img src="screenshots/main.png" width="320" alt="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"> | <img src="screenshots/player_subtitles.png" width="320" alt="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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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<List<Pair<String, String>>>(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)
Expand Down Expand Up @@ -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
},
)
}
}
}
}
}
}

Expand Down
Binary file added screenshots/player_subtitles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.