Thumbnails were blank on the manage screen because the app fetched them from
the static /thumbs/ path, which a front-door botwall on `location /` blocks for
a non-browser client even though /api/ (proxied to the app) works. Add an authed
GET /api/thumb/{slug} that serves the thumbnail JPEG from public/thumbs/, and
have the app fetch from there. Also show the thumbnail in the edit dialog, not
just the list rows.
Construct the photo picker with an explicit maxItems so it reliably opens in
multi-select mode; the no-arg PickMultipleVisualMedia default has been seen to
present single-select on some devices.
157 lines
6.4 KiB
Kotlin
157 lines
6.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.ManageScreen
|
|
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) }
|
|
var showManage by rememberSaveable { mutableStateOf(false) }
|
|
|
|
// Explicit maxItems so the system photo picker reliably opens in
|
|
// multi-select mode (the no-arg default has been seen to present
|
|
// single-select on some devices).
|
|
val picker = rememberLauncherForActivityResult(
|
|
ActivityResultContracts.PickMultipleVisualMedia(maxItems = 50),
|
|
) { 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)
|
|
}
|
|
}
|
|
|
|
when {
|
|
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
|
|
},
|
|
)
|
|
|
|
showManage -> ManageScreen(
|
|
state = viewModel.manage,
|
|
onBack = { showManage = false },
|
|
onRefresh = viewModel::loadManage,
|
|
onSave = viewModel::saveEdit,
|
|
onDelete = viewModel::deletePost,
|
|
onDismissNotice = viewModel::clearManageNotice,
|
|
)
|
|
|
|
else -> PostScreen(
|
|
state = viewModel.post,
|
|
onPickImages = {
|
|
picker.launch(
|
|
PickVisualMediaRequest(
|
|
ActivityResultContracts.PickVisualMedia.ImageOnly,
|
|
),
|
|
)
|
|
},
|
|
onCaptionChange = viewModel::setCaption,
|
|
onTagsChange = viewModel::setTags,
|
|
onDateChange = viewModel::setDate,
|
|
onRemove = viewModel::removeItem,
|
|
onSubmit = viewModel::submit,
|
|
onReset = viewModel::clear,
|
|
onOpenSettings = { showSettings = true },
|
|
onOpenManage = {
|
|
viewModel.loadManage()
|
|
showManage = 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()
|
|
}
|
|
}
|
|
}
|