Pick or share several photos at once and post them in one batch, still against the existing single-image /upload endpoint — no server change. - Unify the post screen on a list model (PickedItem): a single-photo post or share is just a batch of one, so there is one code path. - Multi-select via PickMultipleVisualMedia and multi-share via ACTION_SEND_MULTIPLE (manifest filter added). - Per-photo caption and EXIF-prefilled date; each card shows its own upload status, with a summary and retry-only-failures on re-post. - Enqueue one WorkManager worker per photo, reusing the durable retry queue. UploadQueue now appends a per-item suffix to the slug so a burst enqueued in the same second cannot collide and be dropped by enqueueUniqueWork.
138 lines
5.4 KiB
Kotlin
138 lines
5.4 KiB
Kotlin
package ws.inflo.photogallery
|
|
|
|
import android.Manifest
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.widget.Toast
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.result.PickVisualMediaRequest
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.activity.viewModels
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.core.net.toUri
|
|
import ws.inflo.photogallery.ui.PhotoGalleryTheme
|
|
import ws.inflo.photogallery.ui.PostScreen
|
|
import ws.inflo.photogallery.ui.SettingsScreen
|
|
import ws.inflo.photogallery.ui.UploadViewModel
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
|
|
private val viewModel: UploadViewModel by viewModels()
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
consumeShareIntent(intent)
|
|
|
|
setContent {
|
|
PhotoGalleryTheme {
|
|
// Land on settings until there is somewhere to post to.
|
|
var showSettings by rememberSaveable { mutableStateOf(!viewModel.isConfigured) }
|
|
|
|
val picker = rememberLauncherForActivityResult(
|
|
ActivityResultContracts.PickMultipleVisualMedia(),
|
|
) { uris -> if (uris.isNotEmpty()) viewModel.selectImages(uris) }
|
|
|
|
val notificationPermission = rememberLauncherForActivityResult(
|
|
ActivityResultContracts.RequestPermission(),
|
|
) { /* Upload works either way; this only affects the progress notification. */ }
|
|
|
|
LaunchedEffect(Unit) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
|
ContextCompat.checkSelfPermission(
|
|
this@MainActivity,
|
|
Manifest.permission.POST_NOTIFICATIONS,
|
|
) != PackageManager.PERMISSION_GRANTED
|
|
) {
|
|
notificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
|
|
}
|
|
}
|
|
|
|
if (showSettings) {
|
|
SettingsScreen(
|
|
state = viewModel.config,
|
|
onBaseUrlChange = viewModel::setBaseUrl,
|
|
onUsernameChange = viewModel::setUsername,
|
|
onPasswordChange = viewModel::setPassword,
|
|
onSave = viewModel::saveAndTest,
|
|
onBack = if (viewModel.isConfigured) {
|
|
{ showSettings = false }
|
|
} else {
|
|
null
|
|
},
|
|
)
|
|
} else {
|
|
PostScreen(
|
|
state = viewModel.post,
|
|
onPickImages = {
|
|
picker.launch(
|
|
PickVisualMediaRequest(
|
|
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
|
),
|
|
)
|
|
},
|
|
onCaptionChange = viewModel::setCaption,
|
|
onDateChange = viewModel::setDate,
|
|
onRemove = viewModel::removeItem,
|
|
onSubmit = viewModel::submit,
|
|
onReset = viewModel::clear,
|
|
onOpenSettings = { showSettings = true },
|
|
onOpenUrl = ::openUrl,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
setIntent(intent)
|
|
consumeShareIntent(intent)
|
|
}
|
|
|
|
/** Picks up one or several photos handed over by the share sheet. */
|
|
private fun consumeShareIntent(intent: Intent?) {
|
|
val uris: List<Uri> = when (intent?.action) {
|
|
Intent.ACTION_SEND -> {
|
|
val uri: Uri? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
|
}
|
|
listOfNotNull(uri)
|
|
}
|
|
|
|
Intent.ACTION_SEND_MULTIPLE -> {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
|
} else {
|
|
@Suppress("DEPRECATION")
|
|
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
|
|
}.orEmpty()
|
|
}
|
|
|
|
else -> return
|
|
}
|
|
if (uris.isNotEmpty()) viewModel.selectImages(uris)
|
|
}
|
|
|
|
private fun openUrl(url: String) {
|
|
if (url.isBlank()) return
|
|
try {
|
|
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()))
|
|
} catch (_: Exception) {
|
|
Toast.makeText(this, "No browser to open $url", Toast.LENGTH_SHORT).show()
|
|
}
|
|
}
|
|
}
|