diff --git a/build.gradle b/build.gradle index ec35ae6b..d9a6127c 100644 --- a/build.gradle +++ b/build.gradle @@ -129,6 +129,9 @@ dependencies { implementation 'org.wycliffeassociates:8woc2018-common' implementation 'org.wycliffeassociates:kotlin-resource-container' implementation 'com.github.WycliffeAssociates:jdenticon-kotlin:-SNAPSHOT' + + //Atlassian commonmark (for rendering markdown) + implementation 'com.atlassian.commonmark:commonmark:0.12.1' } //tell gradle what to put in the jar diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/view/ResourceListFragment.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/view/ResourceListFragment.kt new file mode 100644 index 00000000..0ceb7b9c --- /dev/null +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/view/ResourceListFragment.kt @@ -0,0 +1,31 @@ +package org.wycliffeassociates.otter.jvm.app.ui.resources.view + +import org.wycliffeassociates.otter.jvm.app.ui.mainscreen.view.MainScreenStyles +import org.wycliffeassociates.otter.jvm.app.widgets.workbookheader.workbookheader +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.styles.ResourceListStyles +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.view.ResourceListView +import org.wycliffeassociates.otter.jvm.app.ui.resources.viewmodel.ResourcesViewModel +import tornadofx.* + +class ResourceListFragment : Fragment() { + val viewModel: ResourcesViewModel by inject() + + init { + importStylesheet() + importStylesheet() + } + override val root = vbox { + + addClass(MainScreenStyles.main) + + add( + workbookheader { + labelText = "${viewModel.chapter.title} ${messages["resources"]}" + filterText = messages["hideCompleted"] + } + ) + add( + ResourceListView(viewModel.resourceGroups) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/viewmodel/ResourcesViewModel.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/viewmodel/ResourcesViewModel.kt new file mode 100644 index 00000000..ce53dd41 --- /dev/null +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/ui/resources/viewmodel/ResourcesViewModel.kt @@ -0,0 +1,43 @@ +package org.wycliffeassociates.otter.jvm.app.ui.resources.viewmodel + +import javafx.application.Platform +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import org.wycliffeassociates.otter.common.data.workbook.* +import org.wycliffeassociates.otter.common.utils.mapNotNull +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model.ResourceGroupCardItemList +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model.resourceGroupCardItem +import tornadofx.* + +class ResourcesViewModel : ViewModel() { + val activeWorkbookProperty = SimpleObjectProperty() + val workbook: Workbook + get() = activeWorkbookProperty.value + + val activeChapterProperty = SimpleObjectProperty() + val chapter: Chapter + get() = activeChapterProperty.value + + val activeResourceSlugProperty = SimpleStringProperty() + val resourceSlug: String + get() = activeResourceSlugProperty.value + + val resourceGroups: ResourceGroupCardItemList = ResourceGroupCardItemList(mutableListOf()) + + fun loadResourceGroups() { + chapter + .children + .startWith(chapter) + .mapNotNull { resourceGroupCardItem(it, resourceSlug, onSelect = this::navigateToTakesPage) } + .buffer(2) // Buffering by 2 prevents the list UI from jumping while groups are loading + .subscribe { + Platform.runLater { + resourceGroups.addAll(it) + } + } + } + + private fun navigateToTakesPage(resource: Resource) { + // TODO use navigator + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceCardItem.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceCardItem.kt index 537a0b40..92e1786e 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceCardItem.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceCardItem.kt @@ -1,5 +1,45 @@ package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model -data class ResourceCardItem( - val title: String -) +import io.reactivex.disposables.CompositeDisposable +import javafx.beans.property.DoubleProperty +import javafx.beans.property.SimpleDoubleProperty +import org.wycliffeassociates.otter.common.data.workbook.AssociatedAudio +import org.wycliffeassociates.otter.common.data.workbook.Resource +import org.commonmark.parser.Parser +import org.commonmark.renderer.text.TextContentRenderer + +data class ResourceCardItem(val resource: Resource, val onSelect: () -> Unit) { + val title: String = renderTitleAsPlainText() + private val disposables = CompositeDisposable() + val titleProgressProperty: DoubleProperty = resource.titleAudio.progressProperty() + val bodyProgressProperty: DoubleProperty? = resource.bodyAudio?.progressProperty() + val hasBodyAudio: Boolean = resource.bodyAudio != null + + @Suppress("ProtectedInFinal", "Unused") + protected fun finalize() { + clearDisposables() + } + + fun clearDisposables() { + disposables.clear() + } + + private fun AssociatedAudio.progressProperty(): DoubleProperty { + val progressProperty = SimpleDoubleProperty(0.0) + val sub = this.selected.subscribe { + progressProperty.set( if (it.value != null) 1.0 else 0.0) + } + disposables.add(sub) + return progressProperty + } + + companion object { + val parser: Parser = Parser.builder().build() + val renderer: TextContentRenderer = TextContentRenderer.builder().build() + } + + private fun renderTitleAsPlainText(): String { + val document = parser.parse(resource.title.text) + return renderer.render(document) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItem.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItem.kt index bbea70ab..016f3dd3 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItem.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItem.kt @@ -1,8 +1,48 @@ package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model import io.reactivex.Observable +import org.wycliffeassociates.otter.common.data.workbook.* +import tornadofx.* +import tornadofx.FX.Companion.messages data class ResourceGroupCardItem( val title: String, val resources: Observable -) \ No newline at end of file +) { + fun onRemove() { + resources.forEach { + it.clearDisposables() + } + } +} + +fun resourceGroupCardItem(element: BookElement, slug: String, onSelect: (Resource) -> Unit): ResourceGroupCardItem? { + return findResourceGroup(element, slug)?.let { rg -> + ResourceGroupCardItem( + getGroupTitle(element), + getResourceCardItems(rg, onSelect) + ) + } +} + +private fun findResourceGroup(element: BookElement, slug: String): ResourceGroup? { + return element.resources.firstOrNull { + it.info.slug == slug + } +} + +private fun getGroupTitle(element: BookElement): String { + return when (element) { + is Chapter -> "${messages["chapter"]} ${element.title}" + is Chunk -> "${messages["chunk"]} ${element.title}" + else -> element.title + } +} + +private fun getResourceCardItems(rg: ResourceGroup, onSelect: (Resource) -> Unit): Observable { + return rg.resources.map { + ResourceCardItem(it) { + onSelect(it) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItemList.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItemList.kt new file mode 100644 index 00000000..e624788d --- /dev/null +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/model/ResourceGroupCardItemList.kt @@ -0,0 +1,22 @@ +package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model + +import com.sun.javafx.collections.ObservableListWrapper +import javafx.collections.ListChangeListener + +class ResourceGroupCardItemList(list: List) : + ObservableListWrapper(list) { + + init { + addListener( + ListChangeListener { + while(it.next()) { + if (it.wasRemoved()) { + it.removed.forEach { item -> + item.onRemove() + } + } + } + } + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/styles/ResourceListStyles.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/styles/ResourceListStyles.kt new file mode 100644 index 00000000..5f1a941c --- /dev/null +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/styles/ResourceListStyles.kt @@ -0,0 +1,34 @@ +package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.styles + +import javafx.scene.paint.Color +import tornadofx.* + +typealias LinearU = Dimension + +class ResourceListStyles : Stylesheet() { + + companion object { + val resourceGroupList by cssclass() + } + + init { + resourceGroupList { + borderColor += box(Color.TRANSPARENT) // Necessary for border under status bar banner to stay visible + padding = box(0.px, 0.px, 0.px, 80.px) // Left "margin" + scrollBar { + +margin(0.px, 0.px, 0.px, 80.px) // Margin between scrollbar and right side of cards + } + + listCell { + // Add space between the cards (top margin) + // But need to make the "margin" at least as large as the dropshadow offsets + +margin(30.px, 4.px, 0.px, 0.px) + } + } + } + + private fun margin(top: LinearU, right: LinearU, bottom: LinearU, left: LinearU) = mixin { + padding = box(top, right, bottom, left) + backgroundInsets += box(top, right, bottom, left) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceCard.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceCard.kt index eda026b3..e41cb492 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceCard.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceCard.kt @@ -15,8 +15,7 @@ import org.wycliffeassociates.otter.jvm.statusindicator.control.statusindicator import tornadofx.* import tornadofx.FX.Companion.messages -class ResourceCard(private val resource: ResourceCardItem) : HBox() { - +class ResourceCard(private val item: ResourceCardItem) : HBox() { val isCurrentResourceProperty = SimpleBooleanProperty(false) var primaryColorProperty = SimpleObjectProperty(Color.ORANGE) var primaryColor: Color by primaryColorProperty @@ -33,17 +32,18 @@ class ResourceCard(private val resource: ResourceCardItem) : HBox() { add( statusindicator { initForResourceCard() - progress = 1.0 + progressProperty.bind(item.titleProgressProperty) } ) add( statusindicator { initForResourceCard() - progress = 0.0 + item.bodyProgressProperty?.let { progressProperty.bind(it) } + isVisible = item.hasBodyAudio } ) } - text(resource.title) + text(item.title) maxWidth = 150.0 } @@ -59,6 +59,9 @@ class ResourceCard(private val resource: ResourceCardItem) : HBox() { graphic = MaterialIconView(MaterialIcon.APPS, "25px") maxWidth = 500.0 text = messages["viewRecordings"] + action { + item.onSelect() + } } ) } @@ -70,7 +73,6 @@ class ResourceCard(private val resource: ResourceCardItem) : HBox() { trackFill = Color.LIGHTGRAY indicatorRadius = 3.0 } - } fun resourcecard(resource: ResourceCardItem, init: ResourceCard.() -> Unit = {}): ResourceCard { diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCard.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCard.kt index c67f233b..1eed3edf 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCard.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCard.kt @@ -1,19 +1,28 @@ package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.view +import javafx.application.Platform import javafx.scene.layout.VBox import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model.ResourceGroupCardItem import tornadofx.* class ResourceGroupCard(group: ResourceGroupCardItem) : VBox() { + companion object { + const val RENDER_BATCH_SIZE = 10 + } init { importStylesheet() addClass(ResourceGroupCardStyles.resourceGroupCard) label(group.title) - group.resources.subscribe { - add( - resourcecard(it) - ) + + group.resources.buffer(RENDER_BATCH_SIZE).subscribe { items -> + Platform.runLater { + items.forEach { + add( + resourcecard(it) + ) + } + } } } } diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCardStyles.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCardStyles.kt index 843f4be7..4c061be4 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCardStyles.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceGroupCardStyles.kt @@ -6,7 +6,6 @@ import javafx.scene.text.FontWeight import tornadofx.* class ResourceGroupCardStyles : Stylesheet() { - companion object { val resourceGroupCard by cssclass() } diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceListView.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceListView.kt new file mode 100644 index 00000000..b7fc6f99 --- /dev/null +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/app/widgets/resourcecard/view/ResourceListView.kt @@ -0,0 +1,21 @@ +package org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.view + +import javafx.collections.ObservableList +import javafx.scene.control.ListView +import javafx.scene.layout.Priority +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.model.ResourceGroupCardItem +import org.wycliffeassociates.otter.jvm.app.widgets.resourcecard.styles.ResourceListStyles +import tornadofx.* + +class ResourceListView(items: ObservableList): ListView(items) { + init { + cellFormat { + graphic = cache(it.title) { + resourcegroupcard(it) + } + } + vgrow = Priority.ALWAYS + isFocusTraversable = false + addClass(ResourceListStyles.resourceGroupList) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/ResourceRepository.kt b/src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/ResourceRepository.kt index a347ce7e..78217a71 100644 --- a/src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/ResourceRepository.kt +++ b/src/main/kotlin/org/wycliffeassociates/otter/jvm/persistence/repositories/ResourceRepository.kt @@ -6,7 +6,8 @@ import io.reactivex.Single import io.reactivex.rxkotlin.toObservable import io.reactivex.schedulers.Schedulers import jooq.Tables.* -import org.jooq.Condition +import org.jooq.SelectConditionStep +import org.jooq.Record import org.jooq.DSLContext import org.wycliffeassociates.otter.common.collections.multimap.MultiMap import org.wycliffeassociates.otter.common.data.model.Collection @@ -14,6 +15,7 @@ import org.wycliffeassociates.otter.common.data.model.Content import org.wycliffeassociates.otter.common.data.workbook.ResourceInfo import org.wycliffeassociates.otter.common.persistence.repositories.IResourceRepository import org.wycliffeassociates.otter.jvm.persistence.database.AppDatabase +import org.wycliffeassociates.otter.jvm.persistence.database.daos.ContentEntityTable import org.wycliffeassociates.otter.jvm.persistence.database.daos.RecordMappers import org.wycliffeassociates.otter.jvm.persistence.entities.CollectionEntity import org.wycliffeassociates.otter.jvm.persistence.entities.ContentEntity @@ -70,9 +72,8 @@ class ResourceRepository(private val database: AppDatabase) : IResourceRepositor return database.dsl .selectDistinct(DUBLIN_CORE_ENTITY.asterisk()) .from(RESOURCE_LINK) - .join(CONTENT_ENTITY).on(CONTENT_ENTITY.ID.eq(RESOURCE_LINK.CONTENT_FK)) .join(DUBLIN_CORE_ENTITY).on(DUBLIN_CORE_ENTITY.ID.eq(RESOURCE_LINK.DUBLIN_CORE_FK)) - .where(CONTENT_ENTITY.COLLECTION_FK.eq(collection.id)) + .where(RESOURCE_LINK.COLLECTION_FK.eq(collection.id)) .fetch(RecordMappers.Companion::mapToResourceMetadataEntity) .map(this::buildResourceInfo) } @@ -87,32 +88,50 @@ class ResourceRepository(private val database: AppDatabase) : IResourceRepositor .map(this::buildResourceInfo) } - override fun getResources(collection: Collection, resourceInfo: ResourceInfo): Observable { - return getResources({ table -> table.COLLECTION_FK.eq(collection.id) }, resourceInfo) - } - override fun getResources(content: Content, resourceInfo: ResourceInfo): Observable { - return getResources({ table -> table.ID.eq(content.id) }, resourceInfo) + val metadata = mapToResourceMetadataEntity[resourceInfo] + ?: return Observable.empty() + + val main = CONTENT_ENTITY.`as`("main") + val help = CONTENT_ENTITY.`as`("help") + + val selectStatement = database.dsl + .selectDistinct(help.asterisk()) + .from(RESOURCE_LINK) + .join(main).on(main.ID.eq(RESOURCE_LINK.CONTENT_FK)) + .join(help).on(help.ID.eq(RESOURCE_LINK.RESOURCE_CONTENT_FK)) + .where(RESOURCE_LINK.DUBLIN_CORE_FK.eq(metadata.id)) + .and(main.ID.eq(content.id)) + + return getResources(help, selectStatement) } - private fun getResources( - condition: (jooq.tables.ContentEntity) -> Condition, - resourceInfo: ResourceInfo - ): Observable { + /** + * Returns collection-specific resources (does not return resources about the collection's children.) + */ + override fun getResources(collection: Collection, resourceInfo: ResourceInfo): Observable { val metadata = mapToResourceMetadataEntity[resourceInfo] ?: return Observable.empty() - val main = CONTENT_ENTITY.`as`("main") val help = CONTENT_ENTITY.`as`("help") + val selectStatement = database.dsl + .selectDistinct(help.asterisk()) + .from(RESOURCE_LINK) + .join(COLLECTION_ENTITY).on(COLLECTION_ENTITY.ID.eq(RESOURCE_LINK.COLLECTION_FK)) + .join(help).on(RESOURCE_LINK.RESOURCE_CONTENT_FK.eq(help.ID)) + .where(RESOURCE_LINK.DUBLIN_CORE_FK.eq(metadata.id)) + .and(COLLECTION_ENTITY.ID.eq(collection.id)) + + return getResources(help, selectStatement) + } + + private fun getResources( + help: ContentEntityTable, + selectStatement: SelectConditionStep + ): Observable { val contentStreamObservable = Observable.fromCallable { - database.dsl - .selectDistinct(help.asterisk()) - .from(RESOURCE_LINK) - .join(main).on(main.ID.eq(RESOURCE_LINK.CONTENT_FK)) - .join(help).on(help.ID.eq(RESOURCE_LINK.RESOURCE_CONTENT_FK)) - .where(RESOURCE_LINK.DUBLIN_CORE_FK.eq(metadata.id)) - .and(condition(main)) + selectStatement .orderBy(help.START, help.SORT) .fetchStream() .map { RecordMappers.mapToContentEntity(it, help) } diff --git a/src/main/resources/Messages_en.properties b/src/main/resources/Messages_en.properties index bb214511..43ae0469 100644 --- a/src/main/resources/Messages_en.properties +++ b/src/main/resources/Messages_en.properties @@ -11,8 +11,10 @@ load = Load open = Open record = RECORD viewTakes =VIEW TAKES +viewRecordings = VIEW RECORDINGS edit = EDIT chapter = Chapter +chunk = Chunk verse=Verse take=Take select = Select @@ -73,3 +75,5 @@ selectBook = Select a Book home = Home profile = Profile settings = Settings +resources = Resources +hideCompleted = Hide Completed