From 1dfba31265b20a64f2a6be861c455d8b1d643572 Mon Sep 17 00:00:00 2001 From: JP Date: Sat, 30 May 2026 20:26:12 -0700 Subject: [PATCH] Add sample torrents bottom sheet for quick testing - New SampleTorrents.kt with curated list of 4 public domain/CC films - Sample Torrents button (playlist icon) in MagnetInputBar - Bottom sheet dialog displaying torrents with one-tap 'Add' functionality - Each torrent shows title, description, and add button - Dismiss sheet before adding magnet to prevent ANR - Updated MagnetInputBar signature with onSampleTorrents callback - Updated all related composable previews and screenshot tests - Updated README with new feature and project structure - Comprehensive testing: 23/23 unit + UI tests passed on phone portrait/landscape, unfolded foldable, and tablet --- README.md | 6 +- .../simpletorrent/data/SampleTorrents.kt | 30 ++++++++++ .../simpletorrent/ui/main/MainScreen.kt | 60 +++++++++++++++++++ .../jpcottin/simpletorrent/ScreenshotTests.kt | 1 + 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/jpcottin/simpletorrent/data/SampleTorrents.kt diff --git a/README.md b/README.md index 3a7f81b..6920072 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Android NDK and the libtorrent-rasterbar C++ library. ## Features - Add torrents via magnet links or `.torrent` files +- Sample torrents library — curated list of public domain and Creative Commons content for quick testing (Big Buck Bunny, Cosmos Laundromat, Sintel, Tears of Steel) - Create `.torrent` files from any file or folder on the device and seed them immediately - Real-time piece-map visualization (missing / downloaded / actively transferring) - Collapsible per-torrent peer list (top 5 by download speed, refreshed every 3 s) @@ -62,14 +63,15 @@ SimpleTorrent/ │ │ ├── NavigationKeys.kt # Serializable NavKey types: Main, Player(filePath, title) │ │ ├── data/ │ │ │ ├── TorrentManager.kt # Singleton: loads .so, data classes, JNI declarations -│ │ │ └── DataRepository.kt # Interface + DefaultImpl polling every 3 s +│ │ │ ├── DataRepository.kt # Interface + DefaultImpl polling every 3 s +│ │ │ └── SampleTorrents.kt # Curated list of public domain / CC torrents for testing │ │ ├── theme/ │ │ │ ├── Color.kt # Material You color tokens │ │ │ ├── Theme.kt # SimpleTorrentTheme │ │ │ └── Type.kt # Typography scale │ │ └── ui/ │ │ ├── main/ -│ │ │ ├── MainScreen.kt # Compose UI: cards, piece map, file list, peer list +│ │ │ ├── MainScreen.kt # Compose UI: cards, piece map, file list, peer list, sample torrents │ │ │ └── MainScreenViewModel.kt │ │ └── player/ │ │ └── PlayerScreen.kt # ExoPlayer fullscreen player; position saved per file path; auto-detects and loads multi-language subtitles diff --git a/app/src/main/java/com/jpcottin/simpletorrent/data/SampleTorrents.kt b/app/src/main/java/com/jpcottin/simpletorrent/data/SampleTorrents.kt new file mode 100644 index 0000000..3b82826 --- /dev/null +++ b/app/src/main/java/com/jpcottin/simpletorrent/data/SampleTorrents.kt @@ -0,0 +1,30 @@ +package com.jpcottin.simpletorrent.data + +data class SampleTorrent( + val title: String, + val description: String, + val magnet: String, +) + +val SAMPLE_TORRENTS = listOf( + SampleTorrent( + "Big Buck Bunny", + "Animated short film, CC BY 3.0", + "magnet:?xt=urn:btih:dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c&dn=Big+Buck+Bunny&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fbig-buck-bunny.torrent", + ), + SampleTorrent( + "Cosmos Laundromat", + "Blender film, CC BY 3.0", + "magnet:?xt=urn:btih:c9e15763f722f23e98a29decdfae341b98d53056&dn=Cosmos+Laundromat&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fcosmos-laundromat.torrent", + ), + SampleTorrent( + "Sintel", + "Blender film, CC BY 3.0", + "magnet:?xt=urn:btih:08ada5a7a6183aae1e09d831df6748d566095a10&dn=Sintel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel.torrent", + ), + SampleTorrent( + "Tears of Steel", + "Blender film, CC BY 3.0, includes subtitles", + "magnet:?xt=urn:btih:209c8226b299b308beaf2b9cd3fb49212dbd13ec&dn=Tears+of+Steel&tr=udp%3A%2F%2Fexplodie.org%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.empire-js.us%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F&xs=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Ftears-of-steel.torrent", + ), +) diff --git a/app/src/main/java/com/jpcottin/simpletorrent/ui/main/MainScreen.kt b/app/src/main/java/com/jpcottin/simpletorrent/ui/main/MainScreen.kt index 2d03314..6509c40 100644 --- a/app/src/main/java/com/jpcottin/simpletorrent/ui/main/MainScreen.kt +++ b/app/src/main/java/com/jpcottin/simpletorrent/ui/main/MainScreen.kt @@ -39,7 +39,10 @@ import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.PlaylistAdd import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu @@ -77,6 +80,7 @@ import androidx.navigation3.runtime.NavKey import com.jpcottin.simpletorrent.data.DefaultDataRepository import com.jpcottin.simpletorrent.data.FileInfo import com.jpcottin.simpletorrent.data.PeerInfo +import com.jpcottin.simpletorrent.data.SAMPLE_TORRENTS import com.jpcottin.simpletorrent.data.TorrentInfo import com.jpcottin.simpletorrent.data.TorrentManager import com.jpcottin.simpletorrent.theme.SimpleTorrentTheme @@ -94,6 +98,7 @@ fun MainScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() val createState by viewModel.createState.collectAsStateWithLifecycle() var magnetInput by remember { mutableStateOf("") } + var showSampleSheet by remember { mutableStateOf(false) } val context = LocalContext.current fun resolveTreeToPath(uri: android.net.Uri): String? { @@ -214,6 +219,7 @@ fun MainScreen( }, onCreateFile = { fileLauncher.launch(arrayOf("*/*")) }, onCreateFolder = { folderLauncher.launch(null) }, + onSampleTorrents = { showSampleSheet = true }, ) when (state) { @@ -261,6 +267,16 @@ fun MainScreen( } } } + + if (showSampleSheet) { + SampleTorrentsSheet( + onDismiss = { showSampleSheet = false }, + onAdd = { magnet -> + showSampleSheet = false + viewModel.addMagnet(magnet) + }, + ) + } } @Composable @@ -270,6 +286,7 @@ internal fun MagnetInputBar( onAdd: () -> Unit, onCreateFile: () -> Unit, onCreateFolder: () -> Unit, + onSampleTorrents: () -> Unit, modifier: Modifier = Modifier, ) { var showCreateMenu by remember { mutableStateOf(false) } @@ -289,6 +306,9 @@ internal fun MagnetInputBar( keyboardActions = KeyboardActions(onDone = { onAdd() }), ) androidx.compose.material3.Button(onClick = onAdd) { Text("Add") } + IconButton(onClick = onSampleTorrents) { + Icon(Icons.Filled.PlaylistAdd, contentDescription = "Sample torrents") + } Box { IconButton(onClick = { showCreateMenu = true }) { Icon(Icons.Filled.Add, contentDescription = "Create torrent") @@ -310,6 +330,44 @@ internal fun MagnetInputBar( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SampleTorrentsSheet( + onDismiss: () -> Unit, + onAdd: (String) -> Unit, +) { + ModalBottomSheet(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + "Sample Torrents", + fontSize = 18.sp, + modifier = Modifier.padding(bottom = 16.dp), + ) + SAMPLE_TORRENTS.forEach { torrent -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text(torrent.title, fontSize = 14.sp) + Text(torrent.description, fontSize = 12.sp, color = Color.Gray) + } + TextButton(onClick = { onAdd(torrent.magnet) }) { + Text("Add") + } + } + } + } + } +} + private val PLAYABLE_EXTENSIONS = setOf( "mp4", "mkv", "avi", "mov", "webm", "m4v", "ts", "flv", "mp3", "flac", "ogg", "m4a", "aac", "wav", "opus", @@ -724,6 +782,7 @@ private fun MagnetInputBarPreview() { onAdd = {}, onCreateFile = {}, onCreateFolder = {}, + onSampleTorrents = {}, ) } } @@ -738,6 +797,7 @@ private fun MagnetInputBarFilledPreview() { onAdd = {}, onCreateFile = {}, onCreateFolder = {}, + onSampleTorrents = {}, ) } } diff --git a/app/src/screenshotTest/kotlin/com/jpcottin/simpletorrent/ScreenshotTests.kt b/app/src/screenshotTest/kotlin/com/jpcottin/simpletorrent/ScreenshotTests.kt index 6601a33..8e4f02c 100644 --- a/app/src/screenshotTest/kotlin/com/jpcottin/simpletorrent/ScreenshotTests.kt +++ b/app/src/screenshotTest/kotlin/com/jpcottin/simpletorrent/ScreenshotTests.kt @@ -120,6 +120,7 @@ fun MagnetInputBarScreenshot() { onAdd = {}, onCreateFile = {}, onCreateFolder = {}, + onSampleTorrents = {}, ) } }