Simple Board table.

This commit is contained in:
FyloZ 2020-10-08 16:11:29 -04:00
parent ad68d10a3c
commit d6bc50d9f6
28 changed files with 625 additions and 536 deletions

View File

@ -22,10 +22,10 @@ repositories {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.kodein.di:kodein-di:7.0.0")
implementation("org.kodein.di:kodein-di:7.1.0")
implementation("org.kodein.di:kodein-di-generic-jvm:6.5.5")
implementation("org.kodein.di:kodein-di-framework-tornadofx-jvm:7.0.0")
implementation("io.github.microutils:kotlin-logging:1.5.9")
implementation("org.kodein.di:kodein-di-framework-tornadofx-jvm:7.1.0")
implementation("io.github.microutils:kotlin-logging-jvm:2.0.3")
implementation("org.slf4j:slf4j-simple:1.7.26")
// implementation("org.dizitart:potassium-nitrite:3.4.2")
implementation("no.tornado:tornadofx:1.7.20")

View File

@ -1,34 +0,0 @@
package dev.fyloz.plannervio.core.model
import javafx.beans.property.SimpleLongProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
open class Board(
id: Long,
name: String,
description: String,
backgroundImage: BackgroundImage
) {
val idProperty by lazy { SimpleLongProperty(this, "id", id) }
val id by idProperty
val nameProperty by lazy { SimpleStringProperty(this, "name", name) }
val name by nameProperty
val descriptionProperty by lazy { SimpleStringProperty(this, "description", description) }
val description by descriptionProperty
val backgroundImageProperty by lazy { SimpleObjectProperty(this, "backgroundImage", backgroundImage) }
val backgroundImage by backgroundImageProperty
}
fun boardFactory(
id: Long = 0L,
name: String = "board",
description: String = "description",
backgroundImage: BackgroundImage = backgroundImageFactory()
): Board {
return Board(id, name, description, backgroundImage)
}

View File

@ -0,0 +1,57 @@
package dev.fyloz.plannervio.core.model
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleLongProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
import java.time.LocalDateTime
open class Board(
id: Long,
name: String,
description: String,
remote: Boolean = false,
location: String? = null,
private: Boolean = false,
favorite: Boolean = false,
lastVisited: LocalDateTime? = null
) {
val idProperty by lazy { SimpleLongProperty(this, "id", id) }
var id: Long by idProperty
val nameProperty by lazy { SimpleStringProperty(this, "name", name) }
var name: String by nameProperty
val descriptionProperty by lazy { SimpleStringProperty(this, "description", description) }
var description: String by descriptionProperty
val remoteProperty by lazy { SimpleBooleanProperty(this, "remote", remote) }
var isRemote: Boolean by remoteProperty
val locationProperty by lazy { SimpleStringProperty(this, "location", location) }
var location: String? by locationProperty
val privateProperty by lazy { SimpleBooleanProperty(this, "private", private) }
var isPrivate: Boolean by privateProperty
val favoriteProperty by lazy { SimpleBooleanProperty(this, "favorite", favorite) }
var favorite: Boolean by favoriteProperty
val lastVisitedProperty by lazy { SimpleObjectProperty(this, "lastVisited", lastVisited) }
var lastVisited: LocalDateTime? by lastVisitedProperty
}
/** DSL for [Board]s. */
fun board(
id: Long = 0L,
name: String = "board_name",
description: String = "board_description",
remote: Boolean = false,
location: String? = null,
private: Boolean = false,
favorite: Boolean = false,
lastVisited: LocalDateTime? = null,
op: Board.() -> Unit = {}
) = Board(id, name, description, remote, location, private, favorite, lastVisited).apply(op)

View File

@ -1,32 +0,0 @@
package dev.fyloz.plannervio.core.model
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.toObservable
class PrivateBoard(
id: Long,
name: String,
description: String,
backgroundImage: BackgroundImage,
password: String,
users: MutableList<User>
) : Board(id, name, description, backgroundImage) {
val passwordProperty = SimpleStringProperty(this, "password", password)
val password by passwordProperty
val usersProperty = SimpleListProperty(this, "users", users.toObservable())
val users by usersProperty
}
fun privateBoardFactory(
id: Long = 0L,
name: String = "private board",
description: String = "description",
backgroundImage: BackgroundImage = backgroundImageFactory(),
password: String = "password",
users: MutableList<User> = mutableListOf()
): PrivateBoard {
return PrivateBoard(id, name, description, backgroundImage, password, users)
}

View File

@ -1,8 +1,16 @@
package dev.fyloz.plannervio.core.model
data class User(
val username: String,
val email: String,
val password: String
var username: String,
var email: String,
var password: String
) {
}
/** DSL for [User]s. */
fun user(
username: String = "user_username",
email: String = "user_email",
password: String = "user_password",
op: User.() -> Unit = {}
) = User(username, email, password).apply(op)

View File

@ -1,16 +0,0 @@
package dev.fyloz.plannervio.core.model.view
import dev.fyloz.plannervio.core.model.Board
import tornadofx.ItemViewModel
class BoardViewModel(board: Board? = null) : ItemViewModel<Board>() {
init {
if (board != null) item = board
}
val id = bind(Board::idProperty)
val name = bind(Board::nameProperty)
val description = bind(Board::descriptionProperty)
val backgroundImage = bind(Board::backgroundImageProperty)
}

View File

@ -0,0 +1,19 @@
package dev.fyloz.plannervio.core.model.view
import dev.fyloz.plannervio.core.model.Board
import tornadofx.ItemViewModel
open class BoardViewModel(board: Board? = null) : ItemViewModel<Board>() {
init {
if (board != null) item = board
}
val id = bind(Board::idProperty)
val name = bind(Board::nameProperty)
val description = bind(Board::descriptionProperty)
val isRemote = bind(Board::remoteProperty)
val location = bind(Board::locationProperty)
val isPrivate = bind(Board::privateProperty)
val favorite = bind(Board::favoriteProperty)
val lastVisited = bind(Board::lastVisitedProperty)
}

View File

@ -1,80 +1,76 @@
package dev.fyloz.plannervio.core.repository.memory
import dev.fyloz.plannervio.core.model.*
import dev.fyloz.plannervio.core.model.Board
import dev.fyloz.plannervio.core.model.board
import dev.fyloz.plannervio.core.repository.IBoardRepository
import java.time.LocalDateTime
class MemoryBoardRepository : IBoardRepository {
private val boards = mutableListOf<Board>()
init {
save(
boardFactory(
0L,
"Board 1",
"Voluptates facere maiores quis suscipit nostrum. Et accusamus quas animi. Similique reiciendis ea iste cum sunt aut minus. Aut cupiditate consequatur dolor vel doloremque aut eius voluptas. Sed reprehenderit veniam odio voluptatem numquam doloribus. Consequuntur esse assumenda aut expedita voluptatibus.",
BackgroundImage("bundled_background_0.jpg")
)
board {
id = 0L
name = "Board 1"
description = "Voluptates facere maiores quis suscipit nostrum. Et accusamus quas animi. Similique reiciendis ea iste cum sunt aut minus. Aut cupiditate consequatur dolor vel doloremque aut eius voluptas. Sed reprehenderit veniam odio voluptatem numquam doloribus. Consequuntur esse assumenda aut expedita voluptatibus."
favorite = true
lastVisited = LocalDateTime.now()
}
)
save(
boardFactory(
1L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 1L
name = "Board 2"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
}
)
save(
privateBoardFactory(
2L,
"Board 3",
"Pariatur error dolores beatae et rem minus a quia. Facilis nam esse repellat consequatur aperiam nostrum aut vel. Non ratione aut maiores beatae molestiae maxime ipsum molestiae. Asperiores est id in quia tempore soluta voluptatem atque.",
BackgroundImage("bundled_background_3.jpg"),
"password",
mutableListOf(
User("username", "username@email.com", "password")
)
)
board {
id = 2L
name = "Board 3"
description = "Pariatur error dolores beatae et rem minus a quia. Facilis nam esse repellat consequatur aperiam nostrum aut vel. Non ratione aut maiores beatae molestiae maxime ipsum molestiae. Asperiores est id in quia tempore soluta voluptatem atque."
isRemote = true
location = "plannervio.fyloz.dev"
isPrivate = true
}
)
save(
boardFactory(
3L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 3L
name = "Board 4"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
}
)
save(
boardFactory(
4L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 4L
name = "Board 5"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
}
)
save(
boardFactory(
5L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 5L
name = "Board 6"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
isRemote = true
location = "todo.google.ca"
}
)
save(
boardFactory(
6L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 6L
name = "Board 7"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
}
)
save(
boardFactory(
7L,
"test",
"Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non.",
BackgroundImage("bundled_background_1.jpg")
)
board {
id = 7L
name = "Board 8"
description = "Libero rerum sed dolore repudiandae id voluptatem. In iusto nesciunt nihil quia vel ut qui quae. Nihil eos similique corporis fugit autem reiciendis vel. Aut neque quas reprehenderit reiciendis officia non."
}
)
}

View File

@ -1,52 +0,0 @@
package dev.fyloz.plannervio.core.service
import dev.fyloz.plannervio.core.model.BackgroundImage
import javafx.scene.image.Image
import mu.KotlinLogging
import java.io.FileInputStream
import java.io.InputStream
class BackgroundImageService : IBackgroundImageService {
private val logger = KotlinLogging.logger { }
override fun loadImage(image: BackgroundImage): Image? {
val stream = getImageInputStream(image)
return if (stream != null)
Image(stream)
else {
logger.warn("Could not load image '${image.path}'")
null
}
}
/**
* Gets the input stream for a given background image.
*
* @param image The background image
*/
fun getImageInputStream(image: BackgroundImage): InputStream? {
return if (image.bundled) {
getBundledImageInputStream(image)
} else {
getCustomImageInputStream(image)
}
}
/**
* Gets the input stream for a given bundled background image.
*
* @param image The background image
*/
fun getBundledImageInputStream(image: BackgroundImage): InputStream? {
return ClassLoader.getSystemResourceAsStream("images/${image.path}")
}
/**
* Gets the input stream for a given custom background image.
*
* @param image The background image
*/
fun getCustomImageInputStream(image: BackgroundImage): InputStream? {
return FileInputStream(image.path)
}
}

View File

@ -5,11 +5,16 @@ import dev.fyloz.plannervio.core.model.Board
import org.kodein.di.DI
import org.kodein.di.instance
class BoardService(di: DI) : IBoardService {
override val repository: IBoardRepository by di.instance()
/** A service for boards. */
interface IBoardService {
/** Gets an immutable list containing all boards. */
fun getBoards(): List<Board>
}
class BoardServiceImpl(di: DI) : IBoardService {
private val repository: IBoardRepository by di.instance()
override fun getBoards(): List<Board> {
return repository.findAll()
}
}

View File

@ -28,7 +28,7 @@ private const val keywordsServiceKey = "KEYWORDS"
private val keywordsBundle: ResourceBundle = getBundle(keywordsServiceKey.toLowerCase())
private val i18nServices: MutableMap<String, I18nService> = mutableMapOf(
keywordsServiceKey to I18nService(null, keywordsBundle)
// keywordsServiceKey to I18nService(null, keywordsBundle)
)
/**

View File

@ -1,21 +0,0 @@
package dev.fyloz.plannervio.core.service
import dev.fyloz.plannervio.core.model.BackgroundImage
import javafx.scene.image.Image
/**
* A service for background images.
*/
interface IBackgroundImageService {
/**
* Load the JavaFX image for a given BackgroundImage.
*
* If the file cannot be loaded, null will be returned.
*
* @param image The image to load
* @return The JavaFX image for the BackgroundImage
*/
fun loadImage(image: BackgroundImage): Image?
}

View File

@ -1,19 +0,0 @@
package dev.fyloz.plannervio.core.service
import dev.fyloz.plannervio.core.repository.IBoardRepository
import dev.fyloz.plannervio.core.model.Board
/**
* A service for boards.
*/
interface IBoardService {
val repository: IBoardRepository
/**
* Get all boards.
*
* @return An immutable list containing all boards
*/
fun getBoards(): List<Board>
}

View File

@ -1,41 +0,0 @@
package dev.fyloz.plannervio.core.service
import com.beust.klaxon.Klaxon
import com.jfoenix.svg.SVGGlyph
import javafx.scene.paint.Color
import mu.KotlinLogging
import tornadofx.SVGIcon
private val logger = KotlinLogging.logger { }
private val storedSvgPaths: List<SvgPath> = loadSvgPaths()
fun svgGlyph(pathName: SvgPathName, size: Double = 16.0, color: Color = Color.BLACK): SVGGlyph? {
val svgPath = svgPath(pathName) ?: return null
val glyph = SVGGlyph(svgPath.path, color)
glyph.setSize(size, size)
return glyph
}
private fun loadSvgPaths(): List<SvgPath> {
val doc = SvgPath::class.java.getResource("/images/icons.json").readText()
return Klaxon().parseArray(doc) ?: throw IllegalStateException("Could not load svg paths.")
}
private fun svgPath(pathName: SvgPathName): SvgPath? {
val svgPath = storedSvgPaths.firstOrNull {
pathName.pathName == it.name
}
if (svgPath == null) {
logger.warn("Svg path ${pathName.pathName} not loaded or not found")
}
return svgPath
}
private data class SvgPath(val name: String, val path: String)
enum class SvgPathName(val pathName: String) {
FORMAT_LIST_BULLETED("format_list_bulleted"),
STAR("star"),
HISTORY("history"),
COG("cog")
}

View File

@ -2,12 +2,12 @@ package dev.fyloz.plannervio.ui
import dev.fyloz.plannervio.core.repository.IBoardRepository
import dev.fyloz.plannervio.core.repository.memory.MemoryBoardRepository
import dev.fyloz.plannervio.core.service.BackgroundImageService
import dev.fyloz.plannervio.core.service.BoardService
import dev.fyloz.plannervio.core.service.IBackgroundImageService
import dev.fyloz.plannervio.core.service.BoardServiceImpl
import dev.fyloz.plannervio.core.service.IBoardService
import dev.fyloz.plannervio.ui.style.Style
import dev.fyloz.plannervio.ui.view.MainView
import mu.KLogger
import mu.KotlinLogging
import org.kodein.di.DI
import org.kodein.di.DIAware
import org.kodein.di.bind
@ -22,14 +22,18 @@ fun main(args: Array<String>) {
launch<Plannervio>(args)
}
val logger: KLogger by lazy {
System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace")
KotlinLogging.logger(Plannervio::class.simpleName!!)
}
class Plannervio : App(MainView::class, Style::class), DIAware {
override val di: DI
get() = DI {
installTornadoSource()
bind<IBoardRepository>() with singleton { MemoryBoardRepository() }
bind<IBoardService>() with singleton { BoardService(di) }
bind<IBackgroundImageService>() with singleton { BackgroundImageService() }
bind<IBoardService>() with singleton { BoardServiceImpl(di) }
}
init {

View File

@ -8,28 +8,16 @@ import java.util.*
* @param initBundles The bundles contained by the merged bundle
*/
class MergedResourceBundle(vararg initBundles: ResourceBundle) : ResourceBundle() {
private val bundles: MutableList<ResourceBundle>
private val bundles: MutableList<ResourceBundle> = initBundles.toMutableList()
init {
bundles = initBundles.toMutableList()
}
/**
* Adds a bundle to contained bundles.
*
* @param bundle The bundle to add
*/
/** Adds a [bundle] to the contained bundles. */
operator fun plus(bundle: ResourceBundle) {
bundles + bundle
bundles += bundle
}
/**
* Removes a bundle from contained bundles.
*
* @param bundle The bundle to remove
*/
operator fun minus(bundle: ResourceBundle) {
bundles - bundle
/** Adds [bundles] to the contained bundles. */
operator fun plus(bundles: Iterable<ResourceBundle>) {
this.bundles += bundles
}
override fun handleGetObject(key: String): Any? {

View File

@ -1,11 +1,15 @@
package dev.fyloz.plannervio.ui.style
import javafx.geometry.Pos
import javafx.scene.Cursor
import javafx.scene.paint.Color
import javafx.scene.paint.Paint
import javafx.scene.text.TextAlignment
import kfoenix.JFXStylesheet.Companion.jfxButton
import kfoenix.JFXStylesheet.Companion.jfxListView
import kfoenix.JFXStylesheet.Companion.jfxRippler
import kfoenix.JFXStylesheet.Companion.jfxRipplerFill
import kfoenix.JFXStylesheet.Companion.jfxVerticalGap
import tornadofx.*
val primaryTheme = mapOf(
@ -34,6 +38,8 @@ val accentTheme = mapOf(
900 to c("#1a55a4")
)
val background = Color.WHITE
class Style : Stylesheet() {
companion object {
@ -47,8 +53,10 @@ class Style : Stylesheet() {
// Classes
val active by cssclass()
val appNameLabel by cssclass()
val navBar by cssclass()
val centered by cssclass()
val jfxSvgGlyphWrapper by cssclass()
val navBar by cssclass()
val test by cssclass()
}
init {
@ -100,5 +108,44 @@ class Style : Stylesheet() {
}
}
}
tableView {
backgroundColor += background
borderColor += box(c(0.0, 0.0, 0.0, 0.12))
columnHeader {
backgroundColor += background
prefHeight = 56.px
label {
alignment = Pos.CENTER_LEFT
}
}
filler {
backgroundColor += background
}
tableRowCell {
prefHeight = 52.px
borderColor += box(c(0.0, 0.0, 0.0, 0.12), background, background, background)
borderWidth += box(1.px, 0.px, 0.px, 0.px)
backgroundColor += background
and(hover) {
backgroundColor += c(0.0, 0.0, 0.0, 0.04)
}
}
tableColumn {
alignment = Pos.CENTER_LEFT
borderColor += box(Color.TRANSPARENT)
padding = box(0.px, 16.px)
and(centered) {
alignment = Pos.CENTER
}
}
}
}
}

View File

@ -1,18 +1,34 @@
package dev.fyloz.plannervio.ui.view
import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXTextField
import dev.fyloz.plannervio.core.service.SvgPathName
import dev.fyloz.plannervio.core.model.Board
import dev.fyloz.plannervio.core.service.IBoardService
import dev.fyloz.plannervio.ui.style.Style
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.application.Platform
import javafx.beans.binding.Bindings
import javafx.geometry.Pos
import javafx.scene.control.Label
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import kfoenix.jfxtextfield
import org.kodein.di.instance
import org.kodein.di.tornadofx.kodeinDI
import tornadofx.*
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
/** The app's main view */
class MainView : AbstractView() {
class MainView : View() {
private val presenter: MainViewPresenter by inject()
private var activeTabView: View = presenter.activeView
set(value) {
activeTabView.replaceWith(value)
field = value
}
init {
messages = messages("lang.views.main")
}
override val root = borderpane {
left = vbox {
@ -24,24 +40,153 @@ class MainView : AbstractView() {
alignment = Pos.CENTER
text = "Plannervio"
}
this += JFXTextField().apply {
this += jfxtextfield {
vgrow = Priority.NEVER
isLabelFloat = true
promptText = messages["search"] // TODO
promptText = messages[MainViewKeywords.NAV_SEARCH_LABEL]
vboxMargin(horizontal = 16, vertical = 20)
}
this += tabButton(Tab.BOARDS, presenter::onTabButtonClick).apply {
presenter.switchTab(this) // Default tab
for (t in Tab.values()) {
this += tabButton {
tab = t
alignment = Pos.BOTTOM_LEFT
textFill = Color.RED
graphic = hbox {
paddingTop = 2
this += pane {
hboxMargin(top = 8)
addClass(Style.jfxSvgGlyphWrapper)
this += svgGlyph(t.icon, size = 16.0, color = Color.WHITE)
}
this += tabButton(Tab.FAVORITES, presenter::onTabButtonClick)
this += tabButton(Tab.HISTORY, presenter::onTabButtonClick)
this += tabButton(Tab.SETTINGS, presenter::onTabButtonClick)
this += Label(messages[t.messageKey])
}
setOnMouseClicked { presenter.onTabButtonClick(this) }
}
}
}
center = vbox {
isFillWidth = true
padding = insets(10.0)
this += activeTabView
}
}
fun resetCurrentTab() {
val view = presenter.activeView
activeTabView = view
}
}
class BoardsView : View() {
private val presenter by inject<BoardsTabViewPresenter>()
private val headerRowHeightRatio = 1.15
private val rowHeight = 52.0
init {
messages = messages("lang.views.main")
}
private val defaultBoardLocation = messages[Keywords.BOARD_LOCATION_LOCAL]
override val root = tableview(presenter.boards.toObservable()) {
fixedCellSize = rowHeight
prefHeightProperty().bind(fixedCellSizeProperty() * (Bindings.size(items) + headerRowHeightRatio))
columns.setAll(
column(messages[Keywords.BOARD_NAME], String::class) {
minWidth = 150.0
value { it.value.name }
},
column(messages[Keywords.BOARD_LOCATION], String::class) {
value { it.value.location ?: defaultBoardLocation }
minWidth = 150.0
setComparator { a, b ->
when {
a != defaultBoardLocation && b == defaultBoardLocation -> 1
a == defaultBoardLocation && b != defaultBoardLocation -> -1
a == defaultBoardLocation && b == defaultBoardLocation -> 0
else -> a.compareTo(b) * -1
}
}
},
column(messages[Keywords.BOARD_PROTECTION], HBox::class) {
fixedWidth(120.0)
value {
if (it.value.isRemote) {
val private = it.value.isPrivate
LabeledIcon(
if (private) SvgIconName.LOCK else SvgIconName.EARTH,
if (private) messages[Keywords.BOARD_PROTECTION_PRIVATE] else messages[Keywords.BOARD_PROTECTION_PUBLIC]
)
} else null
}
},
column(messages[Keywords.BOARD_FAVORITE], ToggleableIconButton::class) {
fixedWidth(100.0)
value {
toggleableIconButton(SvgIconName.STAR, Color.GOLD, Color.GREY) {
enabled = it.value.favorite
}
}
},
column(messages[Keywords.BOARD_LAST_VISITED], String::class) {
value { it.value.lastVisited?.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)) }
fixedWidth(150.0)
}
)
// Prevent selection
selectionModel.selectedIndexProperty().addListener { _, _, _ -> Platform.runLater { selectionModel.clearSelection() } }
}
}
class FavoritesView : View() {
override val root = pane {
this += Label("FAVORITES")
}
}
class RecentsView : View() {
override val root = pane {
this += Label("RECENTS")
}
}
class SettingsView : View() {
override val root = pane {
this += Label("SETTINGS")
}
}
class MainViewPresenter : Controller() {
private var activeTabButton: TabButton? = null
private val view: MainView by inject()
private val boardSelectionView = find<BoardsView>()
private val favoritesView = find<FavoritesView>()
private val recentView = find<RecentsView>()
private val settingsView = find<SettingsView>()
private val defaultActiveView = boardSelectionView
/** The currently active [TabButton]. **/
var activeTabButton: TabButton? = null
private set
/** The currently active [Tab]. */
private val activeTab: Tab?
get() = activeTabButton?.tab
/** The currently active view. */
val activeView: View
get() {
val tab = activeTab
return if (tab == null) defaultActiveView else when (tab) {
Tab.BOARDS -> boardSelectionView
Tab.SETTINGS -> settingsView
Tab.FAVORITES -> favoritesView
Tab.HISTORY -> recentView
}
}
/** Called when a [TabButton] has been clicked. */
fun onTabButtonClick(button: TabButton) {
@ -54,32 +199,38 @@ class MainViewPresenter : Controller() {
}
/** Switch the active tab to [button]'s tab. */
fun switchTab(button: TabButton) {
private fun switchTab(button: TabButton) {
activeTabButton?.active = false
button.active = true
activeTabButton = button
view.resetCurrentTab()
}
}
/** The [TabButton] is a button linked to a [tab]. */
class TabButton(tab: Tab, active: Boolean = false) : JFXButton() {
/** Specifies the [Tab] linked to the button. */
val tabProperty by lazy { SimpleObjectProperty(tab) }
var tab: Tab by tabProperty
/** Specifies if the button is active. */
val activeProperty = SimpleBooleanProperty(this, "active", active)
var active: Boolean
get() = activeProperty.get()
set(value) {
toggleClass(Style.active, value)
activeProperty.set(value)
}
abstract class TabViewPresenter : Controller() {
protected abstract val view: View
protected val boardService: IBoardService by kodeinDI().instance()
}
enum class Tab(val icon: SvgPathName, val i18nText: String) {
BOARDS(SvgPathName.FORMAT_LIST_BULLETED, "tab.boards.label"),
FAVORITES(SvgPathName.STAR, "tab.favorites.label"),
HISTORY(SvgPathName.HISTORY, "tab.history.label"),
SETTINGS(SvgPathName.COG, "tab.settings.label")
class BoardsTabViewPresenter : TabViewPresenter() {
override val view: BoardsView by inject()
val boards: List<Board>
get() = boardService.getBoards()
}
object MainViewKeywords {
const val NAV_SEARCH_LABEL = "nav.search.label"
const val NAV_TAB_BOARDS_LABEL = "nav.tab.boards.label"
const val NAV_TAB_FAVORITES_LABEL = "nav.tab.favorites.label"
const val NAV_TAB_HISTORY_LABEL = "nav.tab.history.label"
const val NAV_TAB_SETTINGS_LABEL = "nav.tab.settings.label"
const val BOARDS_VIEW_OPEN_LABEL = "view.boards.open.label"
}
enum class Tab(val icon: SvgIconName, val messageKey: String) {
BOARDS(SvgIconName.FORMAT_LIST_BULLETED, MainViewKeywords.NAV_TAB_BOARDS_LABEL),
FAVORITES(SvgIconName.STAR, MainViewKeywords.NAV_TAB_FAVORITES_LABEL),
HISTORY(SvgIconName.HISTORY, MainViewKeywords.NAV_TAB_HISTORY_LABEL),
SETTINGS(SvgIconName.COG, MainViewKeywords.NAV_TAB_SETTINGS_LABEL)
}

View File

@ -1,105 +1,210 @@
package dev.fyloz.plannervio.ui.view
import dev.fyloz.plannervio.core.service.locateI18nService
import dev.fyloz.plannervio.core.service.svgGlyph
import com.beust.klaxon.Klaxon
import com.jfoenix.controls.JFXButton
import com.jfoenix.svg.SVGGlyph
import dev.fyloz.plannervio.ui.Plannervio
import dev.fyloz.plannervio.ui.i18n.MergedResourceBundle
import dev.fyloz.plannervio.ui.logger
import dev.fyloz.plannervio.ui.style.Style
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.event.EventTarget
import javafx.geometry.Pos
import javafx.scene.control.Label
import javafx.scene.Node
import javafx.scene.layout.HBox
import javafx.scene.layout.Pane
import javafx.scene.layout.Region
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import kfoenix.JFXStylesheet.Companion.jfxRipplerFill
import kfoenix.jfxrippler
import tornadofx.*
import java.util.*
/**
* A view. Inheriting this class allows to automatically link the FXML file and the CSS files for the current theme to the fragment.
* If a presenter class is provided, the presenter will also be created.
*
* @constructor Initialize the view, linking the FXML and the CSS files.
*/
abstract class AbstractView : View() {
/**
* Gets the name of the view's files in kebab-case.
*/
val filename by lazy {
javaClass.simpleName
.split("(?=[A-Z])".toRegex())
.map { it.toLowerCase() }
.filter { it.isNotEmpty() }
.filter { it != "view" }
.joinToString("-")
const val KEYWORDS_BUNDLE_NAME = "lang.keywords"
/** A SVG icon. */
data class SvgIcon(val name: String, val author: String, val path: String)
/** The path to the file containing svg icons. */
private const val SVG_ICONS_FILE_PATH = "/images/icons.json"
/** A [List] containing all available [SvgIcon]s. */
val svgIcons = loadSvgIcons()
/** Load all [SvgIcon]s from the file at the path specified by [SVG_ICONS_FILE_PATH]. */
private fun loadSvgIcons(): List<SvgIcon> {
val doc = Plannervio::class.java.getResource(SVG_ICONS_FILE_PATH).readText()
val allIcons: List<SvgIcon> = Klaxon().parseArray(doc) ?: throw IllegalStateException("Could not load SVG icons.")
logger.debug("Loaded ${allIcons.size} SVG icons.")
return allIcons
}
protected val i18nService = locateI18nService(filename)
/** Create a [SVGGlyph] for the given [svgIconName] with the given [size] and [color]. */
fun svgGlyph(svgIconName: SvgIconName, size: Double = 16.0, color: Color = Color.BLACK): SVGGlyph {
val svgIcon = svgIcons.firstOrNull { svgIconName.iconName == it.name }
?: throw IllegalStateException("Cannot create a SVGGlyph for the icon ${svgIconName.iconName} because no icon loaded was found with this name.")
val glyph = SVGGlyph(svgIcon.path, color)
glyph.size = size
return glyph
}
/** A button linked to a [tab]. */
class TabButton(tab: Tab, active: Boolean = false) : JFXButton() {
/** The [Tab] linked to the button. */
val tabProperty by lazy { SimpleObjectProperty(tab) }
var tab: Tab by tabProperty
/** If the button is active. */
val activeProperty = SimpleBooleanProperty(this, "active", active)
var active: Boolean
get() = activeProperty.get()
set(value) {
toggleClass(Style.active, value)
activeProperty.set(value)
}
}
/** A [SvgIcon] and a [Label] wrapped in a [HBox]. */
class LabeledIcon(svgIconName: SvgIconName, text: String, iconSize: Double = 16.0, iconColor: Color = Color.BLACK) : HBox() {
/** The name of the [SvgIcon]. */
val iconNameProperty by lazy { ReadOnlyStringWrapper(svgIconName.iconName) }
val iconName: String by iconNameProperty
/** The text of the [Label]. */
val textProperty by lazy { SimpleStringProperty(text) }
var text: String by textProperty
override fun getUserAgentStylesheet(): String = LabeledIconStyle().base64URL.toExternalForm()
init {
messages = i18nService.bundle
addClass(LabeledIconStyle.labeledIcon)
children.setAll(
svgGlyph(svgIconName, iconSize, iconColor),
label(textProperty)
)
}
// final override val root: Pane = loadFXML("/fxml/views/${filename}.fxml")
private class LabeledIconStyle : Stylesheet() {
companion object {
val labeledIcon by cssclass()
}
/**
* A fragment of the view. Inheriting this class allows to automatically link the FXML file and the CSS files for the current theme to the fragment.
*
* @property view The view which the fragment is linked to.
* @constructor Initialize the fragment, linking the FXML and the CSS files.
*/
abstract class AbstractFragment(val view: AbstractView? = null) : Fragment() {
protected val i18nService = locateI18nService(view?.filename)
init {
messages = i18nService.bundle
labeledIcon {
alignment = Pos.CENTER_LEFT
label {
padding = box(0.px, 0.px, 0.px, 10.px)
}
}
}
}
}
/** Shorthand to create a [TabButton] in a [View]. */
fun View.tabButton(tab: Tab, onClick: (TabButton) -> Unit): TabButton {
return TabButton(tab).apply {
alignment = Pos.BOTTOM_LEFT
textFill = Color.RED
graphic = hbox {
paddingTop = 2
this += pane {
hboxMargin(top = 8)
addClass(Style.jfxSvgGlyphWrapper)
this += svgGlyph(tab.icon, size = 16.0, color = Color.WHITE)!!
/** A toggleable icon [JFXButton], with an [enabledColor] and a [disabledColor]. */
class ToggleableIconButton(svgIconName: SvgIconName, enabledColor: Color, disabledColor: Color, backgroundColor: Color = Color.WHITE, size: Double = 16.0, enabled: Boolean = false) : JFXButton() {
val enabledProperty by lazy { SimpleBooleanProperty(enabled) }
var enabled by enabledProperty
val enabledColorProperty by lazy { SimpleObjectProperty(enabledColor) }
val enabledColor: Color by enabledColorProperty
val disabledColorProperty by lazy { SimpleObjectProperty(disabledColor) }
val disabledColor: Color by disabledColorProperty
private val svgIcon: SVGGlyph
private val iconColor: Color
get() = if (enabled) enabledColor else disabledColor
init {
svgIcon = svgGlyph(svgIconName, size, color = iconColor)
graphic = svgIcon
ripplerFill = backgroundColor
enabledProperty.onChange {
this.enabled = it
toggle()
}
this += Label(messages[tab.i18nText])
}
setOnMouseClicked { onClick(this) }
setOnMouseClicked {
this.enabled = !this.enabled
toggle()
}
}
/** Sets [all] margins of a [Region] contained in a [VBox] to the given size. */
fun Region.vboxMargin(all: Number = 0) {
private fun toggle() {
svgIcon.fill = iconColor
}
}
/** DSL for [TabButton]s. */
fun EventTarget.tabButton(tab: Tab = Tab.BOARDS, op: TabButton.() -> Unit = {}) = TabButton(tab).attachTo(this, op)
/** DSL for [ToggleableIconButton]s. */
fun EventTarget.toggleableIconButton(
svgIconName: SvgIconName,
enabledColor: Color,
disabledColor: Color,
backgroundColor: Color = Color.WHITE,
size: Double = 16.0,
enabled: Boolean = false,
op: ToggleableIconButton.() -> Unit = {}) =
ToggleableIconButton(svgIconName, enabledColor, disabledColor, backgroundColor, size, enabled).attachTo(this, op)
fun messages(vararg names: String): ResourceBundle {
val bundle = MergedResourceBundle(ResourceBundle.getBundle(KEYWORDS_BUNDLE_NAME))
bundle + names.map { ResourceBundle.getBundle(it) }
return bundle
}
/** Sets [all] margins of a [Node] contained in a [VBox] to the given size. */
fun Node.vboxMargin(all: Number = 0) {
this.vboxMargin(all, all)
}
/** Sets [horizontal] and [vertical] margins of a [Region] contained in a [VBox] to the given size. */
fun Region.vboxMargin(horizontal: Number = 0, vertical: Number = 0) {
/** Sets [horizontal] and [vertical] margins of a [Node] contained in a [VBox] to the given size. */
fun Node.vboxMargin(horizontal: Number = 0, vertical: Number = 0) {
this.vboxMargin(vertical, horizontal, vertical, horizontal)
}
/** Sets [top], [right], [bottom] and [left] margins of a [Region] contained in a [VBox] to the given size. */
fun Region.vboxMargin(top: Number = 0, right: Number = 0, bottom: Number = 0, left: Number = 0) {
/** Sets [top], [right], [bottom] and [left] margins of a [Node] contained in a [VBox] to the given size. */
fun Node.vboxMargin(top: Number = 0, right: Number = 0, bottom: Number = 0, left: Number = 0) {
VBox.setMargin(this, insets(top.toDouble(), right.toDouble(), bottom.toDouble(), left.toDouble()))
}
/** Sets [all] margins of a [Region] contained in a [HBox] to the given size. */
fun Region.hboxMargin(all: Number = 0) {
/** Sets [all] margins of a [Node] contained in a [HBox] to the given size. */
fun Node.hboxMargin(all: Number = 0) {
this.hboxMargin(all, all)
}
/** Sets [horizontal] and [vertical] margins of a [Region] contained in a [HBox] to the given size. */
fun Region.hboxMargin(horizontal: Number = 0, vertical: Number = 0) {
/** Sets [horizontal] and [vertical] margins of a [Node] contained in a [HBox] to the given size. */
fun Node.hboxMargin(horizontal: Number = 0, vertical: Number = 0) {
this.hboxMargin(vertical, horizontal, vertical, horizontal)
}
/** Sets [top], [right], [bottom] and [left] margins of a [Region] contained in a [HBox] to the given size. */
fun Region.hboxMargin(top: Number = 0, right: Number = 0, bottom: Number = 0, left: Number = 0) {
/** Sets [top], [right], [bottom] and [left] margins of a [Node] contained in a [HBox] to the given size. */
fun Node.hboxMargin(top: Number = 0, right: Number = 0, bottom: Number = 0, left: Number = 0) {
HBox.setMargin(this, insets(top.toDouble(), right.toDouble(), bottom.toDouble(), left.toDouble()))
}
object Keywords {
const val BOARD_NAME = "board.name"
const val BOARD_DESCRIPTION = "board.description"
const val BOARD_LAST_VISITED = "board.lastVisited"
const val BOARD_FAVORITE = "board.favorite"
const val BOARD_PROTECTION = "board.protection"
const val BOARD_PROTECTION_PRIVATE = "board.protection.private"
const val BOARD_PROTECTION_PUBLIC = "board.protection.public"
const val BOARD_LOCATION = "board.location"
const val BOARD_LOCATION_LOCAL = "board.location.local"
}
/** An enum containing the name of used [SvgIcon]s, for easy access. Not all available icons are specified in this enum. */
enum class SvgIconName(val iconName: String) {
COG("cog"),
EARTH("earth"),
FORMAT_LIST_BULLETED("format-list-bulleted"),
HISTORY("history"),
LOCK("lock"),
STAR("star")
}

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,9 @@
members={0} Members
private=Private
public=Public
name=Name
description=Description
open=OPEN
search=Search...
board.name=Name
board.description=Description
board.lastVisited=Last visit
board.favorite=Favorite
board.protection=Protection
board.location.local=This computer
board.location=Location
board.protection.private=Private
board.protection.public=Public

View File

@ -1,7 +1,9 @@
private=Privé
public=Publique
members={0} Membres
name=Nom
description=Description
open=OUVRIR
search=Recherche...
board.name=Nom
board.description=Description
board.lastVisited=Dernière visite
board.favorite=Favoris
board.protection=Protection
board.location.local=Cet ordinateur
board.location=Emplacement
board.protection.private=Privé
board.protection.public=Publique

View File

@ -1,4 +0,0 @@
tab.boards.label=Boards
tab.favorites.label=Favorites
tab.history.label=Recents
tab.settings.label=Settings

View File

@ -1,4 +0,0 @@
tab.boards.label=Tableaux
tab.favorites.label=Favoris
tab.history.label=Récents
tab.settings.label=Paramètres

View File

@ -0,0 +1,6 @@
nav.search.label=Search...
nav.tab.boards.label=Boards
nav.tab.favorites.label=Favorites
nav.tab.history.label=Recents
nav.tab.settings.label=Settings
view.boards.open.label=Open

View File

@ -0,0 +1,6 @@
nav.search.label=Recherche...
nav.tab.boards.label=Tableaux
nav.tab.favorites.label=Favoris
nav.tab.history.label=Récents
nav.tab.settings.label=Paramètres
view.boards.open.label=Ouvrir

View File

@ -1,67 +0,0 @@
package dev.fyloz.plannervio.core.service
import dev.fyloz.plannervio.core.model.BackgroundImage
import io.mockk.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.io.InputStream
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
class BackgroundImageServiceTest {
private val service: BackgroundImageService = spyk()
private val fakeInputStream: InputStream = mockk()
private val backgroundImage = BackgroundImage("image", true)
@BeforeEach
internal fun setUp() {
clearAllMocks()
}
@Nested
inner class LoadImage {
@Test
fun `Image is loaded`() {
every { service.getImageInputStream(backgroundImage) } returns fakeInputStream
val found = service.loadImage(backgroundImage)
assertNotNull(found)
}
@Test
fun `Image is null`() {
every { service.getImageInputStream(backgroundImage) } returns null
val found = service.loadImage(backgroundImage)
assertNull(found)
}
}
@Nested
inner class GetImageInputStream {
@Test
fun `call getBundledImageInputStream when bundled`() {
every { service.getBundledImageInputStream(backgroundImage) } returns fakeInputStream
val found = service.getImageInputStream(backgroundImage)
verify(exactly = 1) { service.getBundledImageInputStream(backgroundImage) }
assertEquals(fakeInputStream, found)
}
@Test
fun `call getCustomImageInputStream when custom`() {
val customBackgroundImage = BackgroundImage("image", false)
every { service.getCustomImageInputStream(customBackgroundImage) } returns fakeInputStream
val found = service.getImageInputStream(customBackgroundImage)
verify(exactly = 1) { service.getCustomImageInputStream(customBackgroundImage) }
assertEquals(fakeInputStream, found)
}
}
}

View File

@ -1,7 +1,7 @@
package dev.fyloz.plannervio.core.service
import dev.fyloz.plannervio.core.model.Board
import dev.fyloz.plannervio.core.model.boardFactory
import dev.fyloz.plannervio.core.model.board
import dev.fyloz.plannervio.core.repository.IBoardRepository
import io.mockk.clearAllMocks
import io.mockk.every
@ -15,13 +15,13 @@ import org.kodein.di.singleton
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BoardServiceTest {
class BoardServiceImplTest {
private val di: DI = DI {
bind<IBoardRepository>() with singleton { repository }
}
private val repository: IBoardRepository = mockk()
private val service: IBoardService = BoardService(di)
private val service: IBoardService = BoardServiceImpl(di)
@BeforeEach
fun setUp() {
@ -33,8 +33,8 @@ class BoardServiceTest {
@Test
fun `all boards are included`() {
val boards: List<Board> = listOf(
boardFactory(0L),
boardFactory(1L)
board(0L),
board(1L)
)
every { repository.findAll() } returns boards