yes-no-maybe-kink-tool/src/Main.elm

458 lines
12 KiB
Elm
Raw Normal View History

2023-06-23 20:47:15 +00:00
module Main exposing (main)
import FunkyFunktions exposing (..)
2023-07-06 02:13:01 +00:00
import Presets exposing (activities)
import Stickers exposing (Sticker, StickerContent(..), viewSticker)
2023-06-23 20:47:15 +00:00
import Array exposing (Array, get, set)
2023-07-04 00:37:36 +00:00
import Array.Extra as Array
2023-07-06 13:52:17 +00:00
import Base64.Encode as B64E
import Base64.Decode as B64D
2023-06-23 20:47:15 +00:00
import Browser
2023-07-04 16:24:49 +00:00
import Browser.Dom exposing (focus, getViewport, Viewport)
2023-07-06 13:52:17 +00:00
import Browser.Navigation exposing (Key, pushUrl)
import Browser.Events exposing (onMouseMove, onMouseUp, onResize)
2023-07-06 13:52:17 +00:00
import Cbor exposing (CborItem(..))
import Cbor.Encode as CE
import Cbor.Decode as CD
import Html exposing (Attribute, button, div, h3, input, Html, section, text)
2023-07-06 02:13:01 +00:00
import Html.Attributes exposing (class, disabled, id, style, value)
import Html.Attributes.Extra as Attributes exposing (attributeIf)
2023-07-04 21:43:27 +00:00
import Html.Extra exposing (viewMaybe)
import Html.Events exposing (onBlur, onClick, onInput, onMouseDown, onMouseEnter, onMouseLeave)
2023-07-06 03:11:28 +00:00
import Html.Events.Extra exposing (onEnter)
2023-07-06 02:13:01 +00:00
import Html.Lazy exposing (lazy, lazy2)
2023-06-23 20:47:15 +00:00
import Json.Decode as D
import List exposing (map, singleton)
2023-07-06 13:52:17 +00:00
import List.Extra exposing (elemIndex, getAt)
import Maybe exposing (andThen)
import Maybe.Extra exposing (isJust)
2023-07-06 13:52:17 +00:00
import Result exposing (toMaybe)
import String exposing (dropLeft, fromInt, isEmpty, replace, toLower)
2023-07-04 16:24:49 +00:00
import String.Extra exposing (isBlank)
2023-07-05 01:50:32 +00:00
import Svg exposing (path, Svg, svg)
import Svg.Attributes exposing (d, fill, height, stroke, viewBox, width)
2023-06-23 20:47:15 +00:00
import Svg.Events exposing (on)
import Task
import Url exposing (Url)
main = Browser.application
{ init = init
, view = view
, update = update
, onUrlChange = constant Noop
, onUrlRequest = constant Noop
2023-07-04 21:43:27 +00:00
, subscriptions = subscriptions
2023-06-23 20:47:15 +00:00
}
type Msg
= Noop
| Click Int Int Int Int
2023-06-29 02:52:11 +00:00
| SetTileText Int Int String
2023-06-29 03:13:52 +00:00
| AddTile Int
2023-07-06 03:11:28 +00:00
| PruneTile Int Int
| HeldOverNewTile Int Int
2023-07-04 21:43:27 +00:00
| NewMousePos Int Int
| StartDrag Int Int Tile Int Int
| StopDrag
2023-06-23 20:47:15 +00:00
type alias Model =
2023-07-04 22:05:41 +00:00
{ columns: Array Column
2023-07-04 21:43:27 +00:00
, heldTile: Maybe HeldTile
, mousePos: (Int, Int)
2023-07-06 13:52:17 +00:00
, browserKey: Key
}
2023-07-06 13:52:17 +00:00
type alias Column = { tiles: Array Tile }
2023-06-23 20:47:15 +00:00
type alias Tile =
{ text: String
, stickers: List Sticker
}
2023-06-23 20:47:15 +00:00
2023-07-04 21:43:27 +00:00
type alias HeldTile =
{ hoveredColumnIndex: Int
, hoveredTileIndex: Int
, tile: Tile
}
2023-06-29 03:13:52 +00:00
blankTile = { text = "", stickers = [] }
2023-06-23 20:47:15 +00:00
init : () -> Url -> Key -> (Model, Cmd Msg)
init flags url browserKey =
2023-07-06 13:52:17 +00:00
let
m =
case load browserKey url of
Just model -> model
Nothing ->
{ columns = Array.repeat 4 (Column Array.empty)
, mousePos = (200, 200)
, heldTile = Nothing
, browserKey = browserKey
}
in m |> withoutCmd
2023-06-23 20:47:15 +00:00
2023-07-04 21:43:27 +00:00
subscriptions : Model -> Sub Msg
subscriptions model =
if (isJust model.heldTile)
then
Sub.batch
[ onMouseMove <|
D.map2 NewMousePos
(D.field "pageX" D.int)
(D.field "pageY" D.int)
, onMouseUp <| D.succeed StopDrag
]
else Sub.none
2023-07-04 21:43:27 +00:00
2023-06-23 20:47:15 +00:00
view : Model -> Browser.Document Msg
2023-07-04 22:05:41 +00:00
view {columns, heldTile, mousePos} =
2023-07-04 22:11:07 +00:00
[ lazy2 viewColumns heldTile columns
2023-07-06 02:13:01 +00:00
, lazy viewWordbank activities
2023-07-04 21:43:27 +00:00
, viewMaybe (viewFloatyTile mousePos) heldTile
]
|> Browser.Document "meow!"
2023-07-04 22:11:07 +00:00
viewColumns : Maybe HeldTile -> Array Column -> Html Msg
viewColumns heldTile columns =
Array.indexedMap (viewColumn heldTile) columns
|> Array.map (singleton >> div [])
2023-07-04 22:11:07 +00:00
|> Array.toList
|> section
[ class "columns"
]
2023-07-04 22:11:07 +00:00
2023-07-04 21:43:27 +00:00
viewFloatyTile : (Int, Int) -> HeldTile -> Html Msg
viewFloatyTile (mouseX, mouseY) { tile, hoveredColumnIndex } =
2023-07-04 21:43:27 +00:00
div
[ style "top" (mouseY |> String.fromInt |> flip (++) "px")
, style "left" (mouseX |> String.fromInt |> flip (++) "px")
, id "floaty-tile"
, class ((++) "hovering-" <| String.fromInt <| 1 + hoveredColumnIndex)
2023-07-04 21:43:27 +00:00
]
[ viewTile False -1 -1 tile ]
viewHoverTile = div [id "hover-tile"] []
2023-07-04 21:43:27 +00:00
viewColumn : Maybe HeldTile -> Int -> Column -> Html Msg
2023-07-06 13:52:17 +00:00
viewColumn heldOverTile columnIndex {tiles} =
let
isTileHeld = isJust heldOverTile
tileCount = Array.length tiles
onlyTrueActivityTiles = Array.indexedMap (viewTile isTileHeld columnIndex) tiles
trueActivityTilesPlusHoverTile = case heldOverTile of
Nothing -> onlyTrueActivityTiles
2023-07-04 21:43:27 +00:00
Just {hoveredColumnIndex, hoveredTileIndex} ->
if hoveredColumnIndex == columnIndex
then
Array.insertAt
2023-07-04 21:43:27 +00:00
hoveredTileIndex
viewHoverTile
onlyTrueActivityTiles
else onlyTrueActivityTiles
tilesList = Array.toList trueActivityTilesPlusHoverTile
in
div
[ class "column"
, attributeIf isTileHeld (onMouseLeave <| HeldOverNewTile -1 -1)
]
( ( h3
[]
2023-07-06 13:52:17 +00:00
[text <| columnIndexToName columnIndex]
) :: tilesList
++ [ viewAddTile isTileHeld columnIndex tileCount ]
)
2023-07-06 13:52:17 +00:00
columnIndexToName : Int -> String
columnIndexToName i = case i of
0 -> "Into"
1 -> "Willing"
2 -> "Maybe"
3 -> "No"
_ -> "Too Many Columns"
viewTile : Bool -> Int -> Int -> Tile -> Html Msg
viewTile isTileHeld columnIndex tileIndex tile =
div
[ class "tile"
2023-07-05 02:42:08 +00:00
-- , on "click" <| (
-- D.map2 (Click columnIndex tileIndex)
-- (D.field "layerX" D.int)
-- (D.field "layerY" D.int)
-- )
, attributeIf isTileHeld (onMouseEnter <| HeldOverNewTile columnIndex tileIndex)
]
2023-07-06 04:07:33 +00:00
[ viewGrip 24 columnIndex tileIndex tile
2023-07-05 01:50:32 +00:00
, input
2023-06-29 02:52:11 +00:00
[ value tile.text
, onInput (SetTileText columnIndex tileIndex)
2023-07-06 03:11:28 +00:00
, attributeIf (isBlank tile.text) <| onBlur (PruneTile columnIndex tileIndex)
2023-07-04 16:24:49 +00:00
, attributeIf (isBlank tile.text) (id "new-tile")
2023-07-06 03:11:28 +00:00
, onEnter (AddTile columnIndex)
2023-06-29 02:52:11 +00:00
]
[]
, svg
[ Svg.Attributes.class "tile-stickers"
2023-07-06 04:07:33 +00:00
, height "64"
, width "320"
, style "user-select" "none"
2023-06-23 20:47:15 +00:00
]
(map viewSticker tile.stickers)
]
2023-07-06 02:13:01 +00:00
viewGrip : Int -> Int -> Int -> Tile -> Html Msg
viewGrip size colIndx tileIndx tile =
2023-07-05 01:50:32 +00:00
svg
[ viewBox "0 0 103.4 103.4"
2023-07-06 02:13:01 +00:00
, width <| String.fromInt size
, height <| String.fromInt size
2023-07-05 01:50:32 +00:00
, Svg.Attributes.class "drag-gripper"
, on "mousedown" <| (
D.map2 (StartDrag colIndx tileIndx tile)
(D.field "pageX" D.int)
(D.field "pageY" D.int)
)
2023-07-05 01:50:32 +00:00
]
[ path
[ d gripPath
, fill "currentColor"
, stroke "none"
]
[]
]
gripPath = "m17.697 3.3505e-8a17.698 17.698 0 0 0-17.697 17.697 17.698 17.698 0 0 0 17.697 17.699 17.698 17.698 0 0 0 17.699-17.699 17.698 17.698 0 0 0-17.699-17.697zm68 0a17.698 17.698 0 0 0-17.697 17.697 17.698 17.698 0 0 0 17.697 17.699 17.698 17.698 0 0 0 17.699-17.699 17.698 17.698 0 0 0-17.699-17.697zm-68 68a17.698 17.698 0 0 0-17.697 17.697 17.698 17.698 0 0 0 17.697 17.699 17.698 17.698 0 0 0 17.699-17.699 17.698 17.698 0 0 0-17.699-17.697zm68 0a17.698 17.698 0 0 0-17.697 17.697 17.698 17.698 0 0 0 17.697 17.699 17.698 17.698 0 0 0 17.699-17.699 17.698 17.698 0 0 0-17.699-17.697z"
viewAddTile : Bool -> Int -> Int -> Html Msg
viewAddTile isTileHeld columnIndex tileCount =
2023-06-29 03:13:52 +00:00
button
[ class "add-tile"
, class "tile"
, onClick (AddTile columnIndex)
, attributeIf isTileHeld (onMouseEnter <| HeldOverNewTile columnIndex tileCount)
2023-06-29 03:13:52 +00:00
]
[ text "+" ]
2023-06-29 02:52:11 +00:00
2023-07-06 02:13:01 +00:00
viewWordbank : List String -> Html Msg
viewWordbank wordlist =
section
[ id "word-bank"
]
(map viewMiniTile wordlist)
viewMiniTile : String -> Html Msg
viewMiniTile word =
div
[ class "tile"
, class "mini-tile"
]
2023-07-06 04:07:33 +00:00
[ viewGrip 16 -1 -1 { text = word, stickers = [] }
2023-07-06 02:13:01 +00:00
, input
[ value word
, disabled True
]
[]
]
addStickerToTile : Sticker -> Tile -> Tile
addStickerToTile sticker tile =
{ tile | stickers = sticker :: tile.stickers }
2023-06-29 03:13:52 +00:00
addTileToColumn : Column -> Column
addTileToColumn column =
{ column | tiles = Array.push blankTile column.tiles }
isTileEmpty : Tile -> Bool
isTileEmpty = .text >> isEmpty
2023-06-29 02:52:11 +00:00
updateTileText : String -> Tile -> Tile
updateTileText text tile =
{ tile | text = text }
removeTileFromColumn : Int -> Column -> Column
removeTileFromColumn tileIndex column =
{ column | tiles =
Array.removeAt
tileIndex
column.tiles
}
insertTileToColumn : Int -> Tile -> Column -> Column
insertTileToColumn tileIndex tile column =
{ column | tiles =
Array.insertAt
tileIndex
tile
column.tiles
}
2023-06-29 02:52:11 +00:00
modTileInColumn : Int -> (Tile -> Tile) -> Column -> Column
modTileInColumn tileIndex f column =
{ column | tiles =
2023-07-04 00:37:36 +00:00
Array.update
tileIndex
2023-06-29 02:52:11 +00:00
f
column.tiles
}
2023-06-23 20:47:15 +00:00
2023-06-29 02:52:11 +00:00
modColumnInPage : Int -> (Column -> Column) -> Model -> Model
modColumnInPage columnIndex f model =
{ model | columns =
2023-07-04 00:37:36 +00:00
Array.update
2023-06-29 02:52:11 +00:00
columnIndex
f
model.columns
}
2023-06-23 20:47:15 +00:00
update : Msg -> Model -> (Model, Cmd Msg)
2023-06-29 03:30:26 +00:00
update msg model = case Debug.log "UPDATE" msg of
Noop -> (model, Cmd.none)
Click columnIndex tileIndex x y ->
2023-06-29 02:52:11 +00:00
(modColumnInPage columnIndex << modTileInColumn tileIndex)
(addStickerToTile <| Sticker (TextSticker "red" "<3") "Into This" x y)
model
|> withoutCmd
2023-06-29 03:30:26 +00:00
SetTileText columnIndex tileIndex newText ->
2023-06-29 02:52:11 +00:00
(modColumnInPage columnIndex << modTileInColumn tileIndex)
(updateTileText newText)
model
2023-07-06 13:52:17 +00:00
|> withSaveCmd
2023-06-29 03:30:26 +00:00
AddTile columnIndex ->
2023-06-29 03:13:52 +00:00
modColumnInPage columnIndex
addTileToColumn
model
2023-07-04 16:24:49 +00:00
|> withCmd (Task.attempt (constant Noop) (focus "new-tile"))
2023-07-06 03:11:28 +00:00
PruneTile columnIndex tileIndex ->
modColumnInPage columnIndex
2023-07-06 03:11:28 +00:00
(removeTileFromColumn tileIndex)
model
2023-07-06 13:52:17 +00:00
|> withSaveCmd
2023-07-04 21:43:27 +00:00
HeldOverNewTile columnIndex tileIndex -> case model.heldTile of
Nothing -> model |> withoutCmd
2023-07-04 21:43:27 +00:00
Just oldHeldTileInfo ->
let
newTileIndex =
if oldHeldTileInfo.hoveredColumnIndex /= columnIndex
then tileIndex
else if oldHeldTileInfo.hoveredTileIndex == tileIndex
then tileIndex + 1
else tileIndex
in
2023-07-04 21:43:27 +00:00
{ model
| heldTile =
Just
{ oldHeldTileInfo
| hoveredColumnIndex = columnIndex
, hoveredTileIndex = newTileIndex
}
}
|> withoutCmd
NewMousePos x y ->
{ model | mousePos = (x, y) }
|> withoutCmd
StartDrag colIndx tileIndx tile mouseX mouseY ->
let
modelWithTileRemoved =
modColumnInPage colIndx
(removeTileFromColumn tileIndx)
model
in
{ modelWithTileRemoved
| heldTile = Just
{ hoveredColumnIndex = colIndx
, hoveredTileIndex = tileIndx
, tile = tile
}
, mousePos = (mouseX, mouseY)
}
|> withoutCmd
StopDrag ->
case model.heldTile of
Nothing -> model |> withoutCmd
Just {hoveredColumnIndex, hoveredTileIndex, tile} ->
let
modelWithTileInserted =
modColumnInPage hoveredColumnIndex
(insertTileToColumn hoveredTileIndex tile)
model
in
{ modelWithTileInserted | heldTile = Nothing }
2023-07-06 13:52:17 +00:00
|> withSaveCmd
withSaveCmd : Model -> (Model, Cmd Msg)
withSaveCmd model = (model, save model)
andSaveCmd : (Model, Cmd Msg) -> (Model, Cmd Msg)
andSaveCmd (model, cmd) = (model, Cmd.batch [cmd, save model])
save : Model -> Cmd Msg
save model =
saveModel model
|> CE.encode
|> B64E.bytes
|> B64E.encode
|> dropLeft 1
2023-07-06 17:30:05 +00:00
|> replace "=" ""
2023-07-06 13:52:17 +00:00
|> (++) "#"
|> pushUrl model.browserKey
load : Key -> Url -> Maybe Model
load key url =
url.fragment
|> Maybe.map ((++) "g")
2023-07-06 13:52:17 +00:00
|> andThen (B64D.decode B64D.bytes >> toMaybe)
|> andThen (CD.decode (loadModel key))
saveModel : Model -> CE.Encoder
saveModel { columns } =
Array.toList columns
|> map saveColumn
|> CE.sequence
array4 a b c d = Array.fromList [a, b, c, d]
loadModel : Key -> CD.Decoder Model
loadModel key =
CD.map4 array4 loadColumn loadColumn loadColumn loadColumn
|> CD.map Model
|> CD.map ((|>) Nothing)
|> CD.map ((|>) (0, 0))
|> CD.map ((|>) key)
saveColumn : Column -> CE.Encoder
saveColumn { tiles } = CE.list saveTile (Array.toList tiles)
loadColumn : CD.Decoder Column
loadColumn =
CD.list loadTile
|> CD.map Array.fromList
|> CD.map Column
saveTile : Tile -> CE.Encoder
saveTile { text } =
case elemIndex (toLower text) activities of
2023-07-06 13:52:17 +00:00
Just index -> CE.int index
Nothing -> CE.string text
loadTile : CD.Decoder Tile
loadTile =
CD.any
|> CD.andThen loadTileInner
loadTileInner : CborItem -> CD.Decoder Tile
loadTileInner item =
let
word =
case item of
CborInt i ->
case getAt i activities of
Just activity -> CD.succeed activity
Nothing -> CD.fail
CborString s -> CD.succeed s
_ -> CD.fail
in
CD.map (flip Tile []) word