feat: add favicons, drag-to-reorder sites, and SVG support
- Load site favicons (svg → png → ico fallback) via Coil 3 with SubcomposeAsyncImage; globe icon as final fallback - Register SvgDecoder in ImplausiblyApp for SVG favicon support - Add drag-to-reorder via sh.calvin.reorderable library with a drag handle per site row; order persisted to sort_order column - Add sort_order column to stored_sites with schema migration (1.sqm) - New SiteRepository methods: reorderSites(), deleteSitesForInstance(), getAllSites() now includes sort_order - Dependencies: coil-compose, coil-network-okhttp, coil-svg, reorderable (all Apache 2.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9110af7b8f
commit
26467d9047
9 changed files with 248 additions and 36 deletions
|
|
@ -91,6 +91,14 @@ dependencies {
|
||||||
implementation(libs.sqldelight.android.driver)
|
implementation(libs.sqldelight.android.driver)
|
||||||
implementation(libs.sqldelight.coroutines)
|
implementation(libs.sqldelight.coroutines)
|
||||||
|
|
||||||
|
// Coil (favicon loading)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
implementation(libs.coil.svg)
|
||||||
|
|
||||||
|
// Reorderable (drag-to-reorder sites)
|
||||||
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
// Serialization
|
// Serialization
|
||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,19 @@
|
||||||
package no.naiv.implausibly
|
package no.naiv.implausibly
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.PlatformContext
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.svg.SvgDecoder
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class ImplausiblyApp : Application()
|
class ImplausiblyApp : Application(), SingletonImageLoader.Factory {
|
||||||
|
override fun newImageLoader(context: PlatformContext): ImageLoader {
|
||||||
|
return ImageLoader.Builder(context)
|
||||||
|
.components {
|
||||||
|
add(SvgDecoder.Factory())
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,28 @@ class SiteRepository @Inject constructor(
|
||||||
fun getAllSites(): List<Site> {
|
fun getAllSites(): List<Site> {
|
||||||
return database.storedSiteQueries.selectAll()
|
return database.storedSiteQueries.selectAll()
|
||||||
.executeAsList()
|
.executeAsList()
|
||||||
.map { Site(id = it.site_id, instanceId = it.instance_id) }
|
.map { Site(id = it.site_id, instanceId = it.instance_id, sortOrder = it.sort_order.toInt()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSitesForInstance(instanceId: String): List<Site> {
|
fun getSitesForInstance(instanceId: String): List<Site> {
|
||||||
return database.storedSiteQueries.selectByInstance(instanceId)
|
return database.storedSiteQueries.selectByInstance(instanceId)
|
||||||
.executeAsList()
|
.executeAsList()
|
||||||
.map { Site(id = it.site_id, instanceId = it.instance_id) }
|
.map { Site(id = it.site_id, instanceId = it.instance_id, sortOrder = it.sort_order.toInt()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSite(siteId: String, instanceId: String): Site {
|
fun addSite(siteId: String, instanceId: String): Site {
|
||||||
|
val maxOrder = database.storedSiteQueries
|
||||||
|
.maxSortOrderForInstance(instanceId)
|
||||||
|
.executeAsOne()
|
||||||
|
.max_order ?: 0L
|
||||||
|
val nextOrder = maxOrder + 1
|
||||||
|
|
||||||
database.storedSiteQueries.insert(
|
database.storedSiteQueries.insert(
|
||||||
site_id = siteId,
|
site_id = siteId,
|
||||||
instance_id = instanceId
|
instance_id = instanceId,
|
||||||
|
sort_order = nextOrder
|
||||||
)
|
)
|
||||||
return Site(id = siteId, instanceId = instanceId)
|
return Site(id = siteId, instanceId = instanceId, sortOrder = nextOrder.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeSite(siteId: String, instanceId: String) {
|
fun removeSite(siteId: String, instanceId: String) {
|
||||||
|
|
@ -46,7 +53,29 @@ class SiteRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSite(oldSiteId: String, newSiteId: String, instanceId: String) {
|
fun updateSite(oldSiteId: String, newSiteId: String, instanceId: String) {
|
||||||
|
val sites = getSitesForInstance(instanceId)
|
||||||
|
val oldSortOrder = sites.find { it.id == oldSiteId }?.sortOrder?.toLong() ?: 0L
|
||||||
database.storedSiteQueries.delete(site_id = oldSiteId, instance_id = instanceId)
|
database.storedSiteQueries.delete(site_id = oldSiteId, instance_id = instanceId)
|
||||||
database.storedSiteQueries.insert(site_id = newSiteId, instance_id = instanceId)
|
database.storedSiteQueries.insert(
|
||||||
|
site_id = newSiteId,
|
||||||
|
instance_id = instanceId,
|
||||||
|
sort_order = oldSortOrder
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist a new ordering for all sites within an instance.
|
||||||
|
* Called after drag-and-drop reorder.
|
||||||
|
*/
|
||||||
|
fun reorderSites(instanceId: String, orderedSiteIds: List<String>) {
|
||||||
|
database.storedSiteQueries.transaction {
|
||||||
|
orderedSiteIds.forEachIndexed { index, siteId ->
|
||||||
|
database.storedSiteQueries.updateSortOrder(
|
||||||
|
sort_order = index.toLong(),
|
||||||
|
site_id = siteId,
|
||||||
|
instance_id = instanceId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,6 @@ package no.naiv.implausibly.domain.model
|
||||||
*/
|
*/
|
||||||
data class Site(
|
data class Site(
|
||||||
val id: String,
|
val id: String,
|
||||||
val instanceId: String
|
val instanceId: String,
|
||||||
|
val sortOrder: Int = 0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.DragHandle
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
|
@ -38,13 +41,22 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil3.compose.SubcomposeAsyncImage
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.naiv.implausibly.ui.common.LoadingIndicator
|
import no.naiv.implausibly.ui.common.LoadingIndicator
|
||||||
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -124,7 +136,30 @@ fun SitePickerScreen(
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
// Build a lookup from site key to (instanceId, local index within that group)
|
||||||
|
val siteKeyMap = remember(uiState.groups) {
|
||||||
|
buildMap {
|
||||||
|
uiState.groups.forEach { group ->
|
||||||
|
group.sites.forEachIndexed { index, site ->
|
||||||
|
put("site_${group.instance.id}_${site.id}", group.instance.id to index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to ->
|
||||||
|
val fromInfo = siteKeyMap[from.key]
|
||||||
|
val toInfo = siteKeyMap[to.key]
|
||||||
|
// Only reorder within the same instance group
|
||||||
|
if (fromInfo != null && toInfo != null && fromInfo.first == toInfo.first) {
|
||||||
|
viewModel.moveSite(fromInfo.first, fromInfo.second, toInfo.second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
|
|
@ -144,30 +179,41 @@ fun SitePickerScreen(
|
||||||
if (group.isExpanded) {
|
if (group.isExpanded) {
|
||||||
items(
|
items(
|
||||||
items = group.sites,
|
items = group.sites,
|
||||||
key = { "${group.instance.id}_${it.id}" }
|
key = { "site_${group.instance.id}_${it.id}" }
|
||||||
) { site ->
|
) { site ->
|
||||||
val isSelected =
|
val isSelected =
|
||||||
uiState.currentInstanceId == group.instance.id &&
|
uiState.currentInstanceId == group.instance.id &&
|
||||||
uiState.currentSiteId == site.id
|
uiState.currentSiteId == site.id
|
||||||
val isEditing = uiState.editingSiteId == site.id
|
val isEditing = uiState.editingSiteId == site.id
|
||||||
|
|
||||||
SiteRow(
|
ReorderableItem(
|
||||||
siteId = site.id,
|
reorderableLazyListState,
|
||||||
isSelected = isSelected,
|
key = "site_${group.instance.id}_${site.id}"
|
||||||
isEditing = isEditing,
|
) { isDragging ->
|
||||||
editValue = uiState.editSiteValue,
|
SiteRow(
|
||||||
onSelect = {
|
siteId = site.id,
|
||||||
viewModel.selectSite(group.instance.id, site.id)
|
isSelected = isSelected,
|
||||||
onSiteSelected(group.instance.id, site.id)
|
isEditing = isEditing,
|
||||||
},
|
isDragging = isDragging,
|
||||||
onStartEdit = { viewModel.startEditing(site.id) },
|
editValue = uiState.editSiteValue,
|
||||||
onEditValueChanged = viewModel::onEditSiteValueChanged,
|
onSelect = {
|
||||||
onConfirmEdit = { viewModel.confirmEdit(group.instance.id) },
|
viewModel.selectSite(group.instance.id, site.id)
|
||||||
onCancelEdit = viewModel::cancelEdit,
|
onSiteSelected(group.instance.id, site.id)
|
||||||
onDelete = {
|
},
|
||||||
viewModel.requestDeleteSite(group.instance.id, site.id)
|
onStartEdit = { viewModel.startEditing(site.id) },
|
||||||
}
|
onEditValueChanged = viewModel::onEditSiteValueChanged,
|
||||||
)
|
onConfirmEdit = { viewModel.confirmEdit(group.instance.id) },
|
||||||
|
onCancelEdit = viewModel::cancelEdit,
|
||||||
|
onDelete = {
|
||||||
|
viewModel.requestDeleteSite(group.instance.id, site.id)
|
||||||
|
},
|
||||||
|
dragModifier = Modifier.draggableHandle(
|
||||||
|
onDragStopped = {
|
||||||
|
viewModel.saveSiteOrder(group.instance.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline add-site input with validation
|
// Inline add-site input with validation
|
||||||
|
|
@ -260,13 +306,15 @@ private fun SiteRow(
|
||||||
siteId: String,
|
siteId: String,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
|
isDragging: Boolean,
|
||||||
editValue: String,
|
editValue: String,
|
||||||
onSelect: () -> Unit,
|
onSelect: () -> Unit,
|
||||||
onStartEdit: () -> Unit,
|
onStartEdit: () -> Unit,
|
||||||
onEditValueChanged: (String) -> Unit,
|
onEditValueChanged: (String) -> Unit,
|
||||||
onConfirmEdit: () -> Unit,
|
onConfirmEdit: () -> Unit,
|
||||||
onCancelEdit: () -> Unit,
|
onCancelEdit: () -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit,
|
||||||
|
dragModifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -279,7 +327,10 @@ private fun SiteRow(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CardDefaults.cardColors()
|
CardDefaults.cardColors()
|
||||||
}
|
},
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = if (isDragging) 8.dp else 0.dp
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -309,14 +360,13 @@ private fun SiteRow(
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
SiteFavicon(
|
||||||
Icons.Default.Language,
|
siteId = siteId,
|
||||||
contentDescription = null,
|
isSelected = isSelected,
|
||||||
tint = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
modifier = Modifier.size(24.dp)
|
||||||
else MaterialTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -336,6 +386,12 @@ private fun SiteRow(
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Icon(
|
||||||
|
Icons.Default.DragHandle,
|
||||||
|
contentDescription = "Reorder",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = dragModifier
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -393,3 +449,53 @@ private fun AddSiteRow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to load a favicon in order: svg → png → ico.
|
||||||
|
* Falls back to a globe icon if all formats fail.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun SiteFavicon(
|
||||||
|
siteId: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val tint = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
else MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
val urls = remember(siteId) {
|
||||||
|
listOf(
|
||||||
|
"https://$siteId/favicon.svg",
|
||||||
|
"https://$siteId/favicon.png",
|
||||||
|
"https://$siteId/favicon.ico"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var urlIndex by remember(siteId) { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
val globeIcon: @Composable () -> Unit = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Language,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tint,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlIndex < urls.size) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = urls[urlIndex],
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier.clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
loading = { globeIcon() },
|
||||||
|
error = {
|
||||||
|
if (urlIndex < urls.size - 1) {
|
||||||
|
LaunchedEffect(urlIndex) { urlIndex++ }
|
||||||
|
}
|
||||||
|
globeIcon()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
globeIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -268,6 +268,38 @@ class SitePickerViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(editingSiteId = null, editSiteValue = "") }
|
_uiState.update { it.copy(editingSiteId = null, editSiteValue = "") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Site reordering --
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a site within an instance's list. Updates UI state immediately
|
||||||
|
* for responsive drag feedback — persisted on drag end via saveSiteOrder.
|
||||||
|
*/
|
||||||
|
fun moveSite(instanceId: String, fromIndex: Int, toIndex: Int) {
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
groups = state.groups.map { group ->
|
||||||
|
if (group.instance.id == instanceId) {
|
||||||
|
val reordered = group.sites.toMutableList().apply {
|
||||||
|
add(toIndex, removeAt(fromIndex))
|
||||||
|
}
|
||||||
|
group.copy(sites = reordered)
|
||||||
|
} else group
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist the current site order to the database after drag ends.
|
||||||
|
*/
|
||||||
|
fun saveSiteOrder(instanceId: String) {
|
||||||
|
val group = _uiState.value.groups.find { it.instance.id == instanceId } ?: return
|
||||||
|
val orderedIds = group.sites.map { it.id }
|
||||||
|
viewModelScope.launch {
|
||||||
|
siteRepository.reorderSites(instanceId, orderedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist the user's selection so the app opens directly to Dashboard next time.
|
* Persist the user's selection so the app opens directly to Dashboard next time.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
3
app/src/main/sqldelight/migrations/1.sqm
Normal file
3
app/src/main/sqldelight/migrations/1.sqm
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Add sort_order column to stored_sites for user-defined site ordering.
|
||||||
|
-- Default 0 means all existing sites sort alphabetically (fallback to site_id).
|
||||||
|
ALTER TABLE stored_sites ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
@ -3,14 +3,15 @@
|
||||||
CREATE TABLE stored_sites (
|
CREATE TABLE stored_sites (
|
||||||
site_id TEXT NOT NULL,
|
site_id TEXT NOT NULL,
|
||||||
instance_id TEXT NOT NULL,
|
instance_id TEXT NOT NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (site_id, instance_id)
|
PRIMARY KEY (site_id, instance_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
selectByInstance:
|
selectByInstance:
|
||||||
SELECT * FROM stored_sites WHERE instance_id = ? ORDER BY site_id ASC;
|
SELECT * FROM stored_sites WHERE instance_id = ? ORDER BY sort_order ASC, site_id ASC;
|
||||||
|
|
||||||
insert:
|
insert:
|
||||||
INSERT OR IGNORE INTO stored_sites VALUES (?, ?);
|
INSERT OR IGNORE INTO stored_sites(site_id, instance_id, sort_order) VALUES (?, ?, ?);
|
||||||
|
|
||||||
delete:
|
delete:
|
||||||
DELETE FROM stored_sites WHERE site_id = ? AND instance_id = ?;
|
DELETE FROM stored_sites WHERE site_id = ? AND instance_id = ?;
|
||||||
|
|
@ -19,4 +20,10 @@ deleteByInstance:
|
||||||
DELETE FROM stored_sites WHERE instance_id = ?;
|
DELETE FROM stored_sites WHERE instance_id = ?;
|
||||||
|
|
||||||
selectAll:
|
selectAll:
|
||||||
SELECT * FROM stored_sites ORDER BY instance_id ASC, site_id ASC;
|
SELECT * FROM stored_sites ORDER BY instance_id ASC, sort_order ASC, site_id ASC;
|
||||||
|
|
||||||
|
updateSortOrder:
|
||||||
|
UPDATE stored_sites SET sort_order = ? WHERE site_id = ? AND instance_id = ?;
|
||||||
|
|
||||||
|
maxSortOrderForInstance:
|
||||||
|
SELECT MAX(sort_order) AS max_order FROM stored_sites WHERE instance_id = ?;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ ktor = "3.0.3"
|
||||||
# SQLDelight
|
# SQLDelight
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.0.2"
|
||||||
|
|
||||||
|
# Coil
|
||||||
|
coil = "3.0.4"
|
||||||
|
|
||||||
|
# Reorderable
|
||||||
|
reorderable = "2.4.3"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
mockk = "1.13.13"
|
mockk = "1.13.13"
|
||||||
|
|
@ -70,6 +76,14 @@ ktor-client-mock = { group = "io.ktor", name = "ktor-client-mock", version.ref =
|
||||||
sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" }
|
sqldelight-android-driver = { group = "app.cash.sqldelight", name = "android-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-coroutines = { group = "app.cash.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { group = "app.cash.sqldelight", name = "coroutines-extensions", version.ref = "sqldelight" }
|
||||||
|
|
||||||
|
# Coil
|
||||||
|
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" }
|
||||||
|
coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" }
|
||||||
|
coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" }
|
||||||
|
|
||||||
|
# Reorderable
|
||||||
|
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue