Skip to content

Commit

Permalink
Merge pull request #31 from fmasa/feat/export
Browse files Browse the repository at this point in the history
Compendium import & export
  • Loading branch information
fmasa authored Nov 2, 2022
2 parents 0533acc + 4d43273 commit 88fa7da
Show file tree
Hide file tree
Showing 36 changed files with 1,087 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.InputStream
import java.io.OutputStream

@Composable
actual fun rememberFileChooser(
onFileChoose: suspend CoroutineScope.(Result<File>) -> Unit
onFileChoose: suspend CoroutineScope.(Result<ReadableFile>) -> Unit
): FileChooser {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
Expand All @@ -31,20 +32,71 @@ actual fun rememberFileChooser(
onFileChoose(
if (inputStream == null)
Result.failure(Exception("Could not open input stream"))
else Result.success(File(inputStream))
else Result.success(ReadableFile(inputStream))
)
}
}

return AndroidFileChooser(launcher)
}

actual class File(
@Composable
actual fun rememberFileSaver(
type: FileType,
defaultFileName: String,
onLocationChoose: suspend CoroutineScope.(Result<WriteableFile>) -> Unit,
): FileSaver {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current

val contract = ActivityResultContracts.CreateDocument(
when (type) {
FileType.IMAGE -> "image/jpeg"
FileType.PDF -> "application/pdf"
FileType.JSON -> "application/json"
}
)

val launcher = rememberLauncherForActivityResult(contract) { uri ->
coroutineScope.launch(Dispatchers.IO) {
if (uri == null) {
onLocationChoose(Result.failure(Exception("URI not selected")))
return@launch
}

val outputStream = context.contentResolver.openOutputStream(uri)

onLocationChoose(
if (outputStream == null)
Result.failure(Exception("Could not open output stream"))
else Result.success(WriteableFile(outputStream))
)
}
}

return FileSaver {
launcher.launch(defaultFileName)
}
}

actual class ReadableFile(
actual val stream: InputStream
) {
actual fun readBytes(): ByteArray = stream.readBytes()
}

actual class WriteableFile(
private val stream: OutputStream
) {
actual fun writeBytes(bytes: ByteArray) {
stream.write(bytes)
}

actual fun close() {
stream.close()
}
}

class AndroidFileChooser(
private val launcher: ManagedActivityResultLauncher<String, Uri?>
) : FileChooser {
Expand All @@ -53,6 +105,7 @@ class AndroidFileChooser(
when (type) {
FileType.IMAGE -> "image/*"
FileType.PDF -> "application/pdf"
FileType.JSON -> "application/json"
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ import cz.frantisekmasa.wfrp_master.common.compendium.tabs.TalentCompendiumTab
import cz.frantisekmasa.wfrp_master.common.compendium.tabs.TraitCompendiumTab
import cz.frantisekmasa.wfrp_master.common.core.domain.compendium.CompendiumItem
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
import cz.frantisekmasa.wfrp_master.common.core.shared.IO
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.BackButton
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.DialogState
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
import cz.frantisekmasa.wfrp_master.common.core.ui.menu.DropdownMenuItem
import cz.frantisekmasa.wfrp_master.common.core.ui.menu.WithContextMenu
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.ContextMenu
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.FullScreenProgress
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.Spacing
import cz.frantisekmasa.wfrp_master.common.core.ui.primitives.rememberScreenModel
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.OptionsAction
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.Subtitle
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.TopBarAction
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.tabs.TabPager
import cz.frantisekmasa.wfrp_master.common.core.ui.scaffolding.tabs.tab
import cz.frantisekmasa.wfrp_master.common.localization.LocalStrings
Expand Down Expand Up @@ -93,10 +93,25 @@ class CompendiumScreen(
},
navigationIcon = { BackButton() },
actions = {
TopBarAction(
text = strings.buttonImport,
onClick = { navigator.push(CompendiumImportScreen(partyId)) }
)
OptionsAction {
DropdownMenuItem(
onClick = { navigator.push(RulebookCompendiumImportScreen(partyId)) }
) {
Text(strings.buttonImportFromRulebook)
}

DropdownMenuItem(
onClick = { navigator.push(JsonCompendiumImportScreen(partyId)) }
) {
Text(strings.buttonImportFile)
}

DropdownMenuItem(
onClick = { navigator.push(JsonCompendiumExportScreen(partyId)) }
) {
Text(strings.buttonExportFile)
}
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@ import cz.frantisekmasa.wfrp_master.common.compendium.domain.Skill
import cz.frantisekmasa.wfrp_master.common.compendium.domain.Spell
import cz.frantisekmasa.wfrp_master.common.compendium.domain.Talent
import cz.frantisekmasa.wfrp_master.common.compendium.domain.Trait
import cz.frantisekmasa.wfrp_master.common.compendium.import.BlessingImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.CareerImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.CompendiumBundle
import cz.frantisekmasa.wfrp_master.common.compendium.import.MiracleImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.SkillImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.SpellImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.TalentImport
import cz.frantisekmasa.wfrp_master.common.compendium.import.TraitImport
import cz.frantisekmasa.wfrp_master.common.core.domain.compendium.Compendium
import cz.frantisekmasa.wfrp_master.common.core.domain.party.Party
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyRepository
import cz.frantisekmasa.wfrp_master.common.core.utils.right
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

class CompendiumScreenModel(
private val partyId: PartyId,
Expand Down Expand Up @@ -119,4 +132,34 @@ class CompendiumScreenModel(
suspend fun remove(career: Career) {
careerCompendium.remove(partyId, career)
}

suspend fun buildExportJson(): String {
return coroutineScope {
val skills = async { skills.first().map(SkillImport::fromSkill) }
val talents = async { talents.first().map(TalentImport::fromTalent) }
val spells = async { spells.first().map(SpellImport::fromSpell) }
val blessings = async { blessings.first().map(BlessingImport::fromBlessing) }
val miracles = async { miracles.first().map(MiracleImport::fromMiracle) }
val traits = async { traits.first().map(TraitImport::fromTrait) }
val careers = async { careers.first().map(CareerImport::fromCareer) }

val bundle = CompendiumBundle(
skills = skills.await(),
talents = talents.await(),
spells = spells.await(),
blessings = blessings.await(),
miracles = miracles.await(),
traits = traits.await(),
careers = careers.await(),
)

json.encodeToString(serializer(), bundle)
}
}

companion object {
private val json = Json {
encodeDefaults = true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import cz.frantisekmasa.wfrp_master.common.compendium.domain.Talent
import cz.frantisekmasa.wfrp_master.common.compendium.domain.Trait
import cz.frantisekmasa.wfrp_master.common.core.domain.compendium.CompendiumItem
import cz.frantisekmasa.wfrp_master.common.core.domain.party.PartyId
import cz.frantisekmasa.wfrp_master.common.core.shared.IO
import cz.frantisekmasa.wfrp_master.common.core.ui.buttons.CloseButton
import cz.frantisekmasa.wfrp_master.common.core.ui.dialogs.FullScreenDialog
import cz.frantisekmasa.wfrp_master.common.core.ui.flow.collectWithLifecycle
Expand Down Expand Up @@ -61,7 +60,6 @@ internal fun ImportDialog(
is ImportDialogState.PickingItemsToImport -> ImportedItemsPicker(
screenModel = screenModel,
state = state,
partyId = partyId,
onDismissRequest = onDismissRequest,
onComplete = onComplete,
)
Expand All @@ -72,7 +70,6 @@ internal fun ImportDialog(
@Composable
private fun ImportedItemsPicker(
screenModel: CompendiumScreenModel,
partyId: PartyId,
state: ImportDialogState.PickingItemsToImport,
onDismissRequest: () -> Unit,
onComplete: () -> Unit,
Expand All @@ -90,6 +87,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.TALENTS },
onClose = onDismissRequest,
existingItems = screenModel.skills,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.TALENTS -> {
Expand All @@ -100,6 +98,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.SPELLS },
onClose = onDismissRequest,
existingItems = screenModel.talents,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.SPELLS -> {
Expand All @@ -110,6 +109,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.BLESSINGS },
onClose = onDismissRequest,
existingItems = screenModel.spells,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.BLESSINGS -> {
Expand All @@ -120,6 +120,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.MIRACLES },
onClose = onDismissRequest,
existingItems = screenModel.blessings,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.MIRACLES -> {
Expand All @@ -130,6 +131,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.TRAITS },
onClose = onDismissRequest,
existingItems = screenModel.miracles,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.TRAITS -> {
Expand All @@ -140,6 +142,7 @@ private fun ImportedItemsPicker(
onContinue = { screen = ItemsScreen.CAREERS },
onClose = onDismissRequest,
existingItems = screenModel.traits,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
ItemsScreen.CAREERS -> {
Expand All @@ -150,6 +153,7 @@ private fun ImportedItemsPicker(
onContinue = onComplete,
onClose = onDismissRequest,
existingItems = screenModel.careers,
replaceExistingByDefault = state.replaceExistingByDefault,
)
}
}
Expand All @@ -163,6 +167,7 @@ private fun <T : CompendiumItem<T>> ItemPicker(
onContinue: () -> Unit,
existingItems: Flow<List<T>>,
items: List<T>,
replaceExistingByDefault: Boolean,
) {
val existingItemsList = existingItems.collectWithLifecycle(null).value

Expand All @@ -179,12 +184,13 @@ private fun <T : CompendiumItem<T>> ItemPicker(
return
}

val existingItemNames = remember(existingItemsList) {
existingItemsList.map { it.name }.toHashSet()
val existingItemsByName = remember(existingItemsList) {
existingItemsList.associateBy { it.name }
}

val selectedItems = remember(items, existingItemNames) {
items.map { it.id to !existingItemNames.contains(it.name) }.toMutableStateMap()
val selectedItems = remember(items, existingItemsByName, replaceExistingByDefault) {
items.map { it.id to (replaceExistingByDefault || it.name !in existingItemsByName) }
.toMutableStateMap()
}
val atLeastOneSelected = selectedItems.containsValue(true)

Expand Down Expand Up @@ -212,7 +218,20 @@ private fun <T : CompendiumItem<T>> ItemPicker(

if (atLeastOneSelected) {
withContext(Dispatchers.IO) {
onSave(items.filter { selectedItems.contains(it.id) })
onSave(
items
.asSequence()
.filter { selectedItems[it.id] == true }
.map {
val existingItem = existingItemsByName[it.name]

if (existingItem != null)
it.replace(existingItem)
else it
}
.distinctBy { it.id }
.toList()
)
}
}

Expand Down Expand Up @@ -253,8 +272,14 @@ private fun <T : CompendiumItem<T>> ItemPicker(
onValueChange = { selectedItems[item.id] = it },
),
text = { Text(item.name) },
secondaryText = if (existingItemNames.contains(item.name)) {
{ Text(strings.compendium.messages.itemAlreadyExists) }
secondaryText = if (item.name in existingItemsByName) {
{
Text(
if (selectedItems[item.id] == true)
strings.compendium.messages.willReplaceExistingItem
else strings.compendium.messages.itemAlreadyExists
)
}
} else null
)
}
Expand All @@ -265,7 +290,7 @@ private fun <T : CompendiumItem<T>> ItemPicker(
}

@Immutable
internal sealed class ImportDialogState {
sealed class ImportDialogState {
@Immutable
object LoadingItems : ImportDialogState()

Expand All @@ -278,6 +303,7 @@ internal sealed class ImportDialogState {
val miracles: List<Miracle>,
val traits: List<Trait>,
val careers: List<Career>,
val replaceExistingByDefault: Boolean,
) : ImportDialogState()
}

Expand Down
Loading

0 comments on commit 88fa7da

Please sign in to comment.