diff --git a/android/tinySSB/app/build.gradle b/android/tinySSB/app/build.gradle index 420aa20..021680d 100644 --- a/android/tinySSB/app/build.gradle +++ b/android/tinySSB/app/build.gradle @@ -12,7 +12,7 @@ android { defaultConfig { applicationId "nz.scuttlebutt.tremolavossbol" - minSdk 23 + minSdk 24 targetSdkVersion 30 versionCode 1 versionName "0.1" diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg new file mode 100644 index 0000000..9748a15 Binary files /dev/null and b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/BS-Icon.jpeg differ diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battelship/battleship.svg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/battleship.svg similarity index 100% rename from android/tinySSB/app/src/main/assets/web/games/dpi24-06-battelship/battleship.svg rename to android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/battleship.svg diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css new file mode 100644 index 0000000..2407354 --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.css @@ -0,0 +1,341 @@ +/* + games/dpi24-06-battleship/duel.css +*/ + +/* -------------DUEL-------------------*/ +#duelInviteContainer { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #ffffff; + padding: 20px; + border: 2px solid #000000; + z-index: 1000; + } + +/* +.scroll-container { + display: flex; + flex-direction: column; + align-items: flex-start; + overflow-y: auto; + max-height: 350px; +} +*/ + +.square { + width: 150px; + height: 150px; + margin: 10px 0; + padding: 0; + border: none; + background: none; + cursor: pointer; +} + +.square img { + width: 100%; + height: 100%; + object-fit: cover; +} + +button.square:focus { + outline: none; /* removes focus */ +} + +button.square:active { + transform: scale(0.95); /* makes button smaller when clicked */ +} + +.duel-button-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.duel-button { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + max-width: 550px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; + text-align: left; + cursor: pointer; + transition: background-color 0.3s; +} + +.no-duel-box { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 600px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #f9f9f9; + text-align: center; + cursor: default; /* cursor set to inactive */ +} + +.duel-button-invited { + background-color: #d1ecf1; /* light blue */ +} + +.duel-button-running { + background-color: #fff3cd; /* light green */ +} + +.duel-button-waiting { + background-color: #fff3cd; /* light yellow */ +} + +.duel-button-won { + background-color: #c3e6cb; /* green */ +} + +.duel-button-lost { + background-color: #f8d7da; /* light red */ +} + +.duel-button-stopped { + background-color: #e2e3e5; /* light grey */ +} + +.duel-button:hover { + background-color: #e0e0e0; +} + +.duel-image { + width: 50px; + height: 50px; + margin-right: 10px; +} + +.duel-text { + font-size: 18px; + font-weight: bold; +} + +.board { + display: flex; + justify-content: center; + align-items: center; +} + +.battleshipsTopRow { + background: #50A4D3; + color: white; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; +} + +.battleshipsLeftRow { + background: #50A4D3; + color: white; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; +} + +.displays { + width: 40vh; +} + +.grid { + aspect-ratio: 1 / 1; + display: grid; + grid-template-columns: repeat(11, 1fr); + grid-template-rows: repeat(11, 1fr); + +} + +.field { + background: #ACCADF; + border-right: 1px solid Blue; + border-top: 1px solid Blue; + display: flex; + justify-content: center; + align-items: center; +} + +.hole { + width: 30%; + height: 30%; + border: 1px solid Blue; + border-radius: 50%; +} + +.ship { + background: Black; + border-right: 1px solid Blue; + border-top: 1px solid Blue; + display: flex; + justify-content: center; + align-items: center; +} + +.miss { + background: Blue; + border-right: 1px solid Blue; + border-top: 1px solid Blue; + display: flex; + justify-content: center; + align-items: center; +} + +.hit { + background: Orange; + border-right: 1px solid Blue; + border-top: 1px solid Blue; + display: flex; + justify-content: center; + align-items: center; +} + +.sunken { + background: Red; + border-right: 1px solid Blue; + border-top: 1px solid Blue; + display: flex; + justify-content: center; + align-items: center; +} + +.field_clicked { + -webkit-animation: grow 0.5s; +} + +@-webkit-keyframes grow { + from { + width: 0%; + height: 0%; + } + to { + width: 30%; + height: 30%; + } +} + +.battleships_control { + display: flex; + justify-content: center; +} + +.battleships_info { + margin-top: 10px; + width: 70%; + padding: 15px 25px; + font-size: 1.5em; + text-align: center; + outline: none; + color: #fff; + background-color: #4CAF50; + border: none; + border-radius: 15px; +} + +.battleships_placer { + display: flex; + justify-content: center; +} + +.battleships_length { + width: 35vw; + text-align: center; + font-size: 1.3em; + padding: 15px; + margin-top: 10px; + background-color: #00c9ff; + border: none; + border-radius: 15px; + color: #fff; +} + +.battleships_turn { + width: 70vw; + text-align: center; + font-size: 1.3em; + padding: 15px; + margin-top: 10px; + border: 1px solid #000; + border-radius: 15px; + color: #fff; +} + +.turn-won { + background-color: green; + color: white; +} + +.turn-lost { + background-color: red; + color: white; +} + +.turn-your { + background-color: d4edda; + color: white; +} + +.turn-enemy { + background-color: f8d7da; + color: white; +} + +.turn-default { + background-color: #50A4D3; + color: #fff; +} + +.battleships_orientation_button { + margin-top: 10px; + width: 35vw; + padding: 15px; + font-size: 1.3em; + text-align: center; + outline: none; + color: #fff; + background-color: #4CAF50; + border: none; + border-radius: 15px; + box-shadow: 0 9px #999; +} + +.battleships_orientation_button:active { + background-color: #3e8e41; + box-shadow: 0 5px #666; + transform: translateY(4px); +} + +.battleships_big_button { + margin-top: 10px; + width: 70%; + padding: 15px 25px; + font-size: 1.5em; + text-align: center; + outline: none; + color: #fff; + background-color: #4CAF50; + border: none; + border-radius: 15px; + box-shadow: 0 9px #999; +} + +.battleships_big_button:active { + background-color: #3e8e41; + box-shadow: 0 5px #666; + transform: translateY(4px); +} + +/* eof */ \ No newline at end of file diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js new file mode 100644 index 0000000..1afa24a --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/duel.js @@ -0,0 +1,562 @@ +"use strict"; + +var battleships_turn = false +var battleships_horizontal = true +var battleships_ship_positions = "" +var battleship_ship_lengths = [2, 3, 3, 4 ,5] +var battleship_status = "STOPPED" +var battleship_timestamp = "0" + +var owner = "-" +var peer = "-" +var game = "-" + +/* +* This method opens the selection menu for possible games needed in order to send an invite. +*/ + +function duel_openDuels() { + return; + console.log('duel_openDuels()') + setScenario('duels') + /* + // closeOverlay(); + var duelInviteContainer = document.getElementById('div:duelInviteContainer'); + if (duelInviteContainer) { + duelInviteContainer.style.display = 'block'; + } else { + console.error('Duel-Invite-Container not found!'); + } + */ +} + +/** +* This method gets called if you click on an icon in the invitemenu. +*/ +function inviteForDuel(gameType) { + closeDuelOverlay(); + var stringToEncode = "BSH INV " + myId; + console.log('Invited battleship ' + JSON.stringify(stringToEncode)) + backend("games "+ stringToEncode); +} + +/** +* This function closes the invitemenu. It gets called by the Cancel-Button. +*/ +function closeDuelOverlay() { + var duelInviteContainer = document.getElementById('div:duelInviteContainer'); + if (duelInviteContainer) { + duelInviteContainer.style.display = 'none'; + } +} + +// ---------- GAME-SCREEN ----------- + +function update_game_gui(response) { + console.log("BSH updating GUI ... " + JSON.stringify(response)) + if (curr_scenario == 'battleships') { + if (window.GamesHandler && typeof window.GamesHandler.getInstanceDescriptorFromFids === 'function' && typeof window.GamesHandler.getInstanceDescriptorFromFid === 'function') { + var instanceDescriptor = "" + var responseList = response.split(" ") + console.log("BSH updating GUI after split") + if (responseList.length > 1) { + console.log("BSH updating GUI > 1") + if (owner == responseList[0] && peer == "-" && battleship_status == "INVITED") { + console.log("BSH updating GUI setting peer") + peer = responseList[1] + } + } + console.log("BSH updating GUI before instanceDescriptor") + if (peer == "-") { + instanceDescriptor = window.GamesHandler.getInstanceDescriptorFromFid(game, owner) + } else { + instanceDescriptor = window.GamesHandler.getInstanceDescriptorFromFids(game, owner, peer, battleship_timestamp) + } + + console.log("BSH update_gui ", JSON.stringify(instanceDescriptor)) + var instanceList = instanceDescriptor.split(" ") + battleship_status = instanceList[4] + battleship_timestamp = instanceList[3] + if (instanceList.length < 6) { return } + var playerTurn = instanceList[5] + if (playerTurn == "1") { + battleships(true, instanceList[6]) + } else { + battleships(false, instanceList[6]) + } + } + } else if (curr_scenario == 'duels') { + show_duels() + } +} + + +// The main function that launches the game GUI +function battleships(turn, ships_fired_recv) { + var args = ships_fired_recv.split("^"); + battleships_turn = turn; + console.log("Called BSH GUI:", JSON.stringify(ships_fired_recv)); + + setScenario("battleships") + + /* + var c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "
My Battleships
"; + */ + battleships_load_config(battleship_status, args[0], args[1], args[2]); +} + +/** +* This function is called if a user wants to quit the game. +*/ +function quit_bsh() { + backend("games BSH DUELQUIT " + owner + " " + peer); + reset_battleship_mode(); + show_duels() +} + + +// Gets called when the user clicks on a square of the bottom field +function battleship_bottom_field_click(i) { + console.log("BSH registered Shot: ", JSON.stringify(i)) + var square = document.getElementById("battleships:bottom" + i) + // Do nothing if it is not our turn + if (!battleships_turn) { + return + } else { + backend("games BSH SHOT " + owner + " " + peer + " " + (((i % 10) + 9) % 10) + "" + Math.floor((i - 1) / 10)) + battleships_set_turn(false) + battleships_show_turn() + } + square.childNodes[0].className = "hole field_clicked" + setTimeout(function () { + square.childNodes[0].className = "hole" + }, 100); +} + +function battleship_top_field_click(i) { + // ignore +} + +// Generates the HTML of the playing field +function battleships_setup() { + console.log("BSH_setup()", JSON.stringify(battleship_status)); + battleships_hide_controls() + var board = document.getElementById("battleships:board") + var topgrid = document.getElementById("battleships:topgrid") + topgrid.innerHTML = "" + topgrid.insertAdjacentHTML("beforeend", "") + for (var i = 1; i < 11; i++) { + topgrid.insertAdjacentHTML("beforeend", "" + i + "") + } + var counter = 1; + for (var i = 12; i <= 121; i++) { + if ((i % 11) - 1 == 0) { + topgrid.insertAdjacentHTML("beforeend", "" + String.fromCharCode(64 + (i - 1) / 11).toUpperCase() + "") + } else { + topgrid.insertAdjacentHTML("beforeend", "
  • ") + counter++ + } + } + var bottomgrid = document.getElementById("battleships:bottomgrid") + bottomgrid.innerHTML = "" + bottomgrid.insertAdjacentHTML("beforeend", "") + for (var i = 1; i < 11; i++) { + bottomgrid.insertAdjacentHTML("beforeend", "" + i + "") + } + var counter = 1; + for (var i = 12; i <= 121; i++) { + if ((i % 11) - 1 == 0) { + bottomgrid.insertAdjacentHTML("beforeend", "" + String.fromCharCode(64 + (i - 1) / 11).toUpperCase() + "") + } else { + bottomgrid.insertAdjacentHTML("beforeend", "
  • ") + counter++ + } + } +} + +function battleships_set_turn(is_turn) { + battleships_turn = is_turn +} + +// Hides everything below the two grids +function battleships_hide_controls() { + document.getElementById("battleships:turn").style.display = "none" +} + +// Shows the turn indicator below the grid +function battleships_show_turn() { + console.log("BSH Showing Turn..."); + battleships_hide_controls(); + var peerId = myId; + var turn = document.getElementById("battleships:turn"); + turn.style.display = null; + + console.log("BSH Determining what to display as turn ..."); + + // Set default styles + turn.style.width = '70vw'; + turn.style.textAlign = 'center'; + turn.style.fontSize = '1.3em'; + turn.style.padding = '15px'; + turn.style.marginTop = '10px'; + turn.style.border = '1px solid #000'; + turn.style.borderRadius = '15px'; + turn.style.backgroundColor = '#50A4D3'; + turn.style.color = '#fff'; + + if (peerId == owner || peerId == peer) { + if (battleship_status == "WON") { + turn.innerHTML = "You Won!"; + turn.style.backgroundColor = 'green'; + turn.style.color = 'white'; + } else if (battleship_status == "LOST") { + turn.innerHTML = "You Lost!"; + turn.style.backgroundColor = 'red'; + turn.style.color = 'white'; + } else if (battleship_status == "STOPPED") { + turn.innerHTML = "The game is stopped!"; + } else if (battleship_status == "INVITED") { + console.log("BSH invite-button init ..."); + turn.innerHTML = "Waiting for other!"; + } else if (battleship_status == "WAITING") { + if (peerId == "-") { + turn.innerHTML = "Waiting ..."; + } + } else if (battleship_status == "RUNNING") { + if (battleships_turn) { + turn.innerHTML = "Your Turn"; + turn.style.backgroundColor = '#d4edda'; // Light green + turn.style.color = 'black'; + } else { + turn.innerHTML = "Enemy Turn"; + turn.style.backgroundColor = '#f8d7da'; // Light red + turn.style.color = 'black'; + } + } + } else { + if (battleship_status == "WON") { + if (battleships_turn) { + turn.innerHTML = "Owner has Won!"; + } else { + turn.innerHTML = "Peer has Won!"; + } + turn.style.backgroundColor = 'green'; + turn.style.color = 'white'; + } else if (battleship_status == "LOST") { + if (battleships_turn) { + turn.innerHTML = "Owner has Lost!"; + } else { + turn.innerHTML = "Peer has Lost!"; + } + turn.style.backgroundColor = 'red'; + turn.style.color = 'white'; + } else { + if (battleships_turn) { + turn.innerHTML = "Owner's Turn!"; + } else { + turn.innerHTML = "Peer's Turn!"; + } + } + } +} + + +// Displays the config given from the backend. The format of the config is +// well described in the backend. +function battleships_load_config(state, ships, deliv, recv) { + document.getElementById("conversationTitle").innerHTML = "
    My Battleships
    "; + battleships_ship_positions = "" + console.log("BSH_load_config", JSON.stringify(state + " " + ships + " " + recv + " " + deliv)); + if (state === "STOPPED") { + document.getElementById("conversationTitle").innerHTML = "
    Stopped Game!
    "; + battleships_setup() + battleships_show_turn() + } else if (state === "INVITED") { + battleships_setup() + battleships_show_turn() + //return + } else if (state === "WON") { + document.getElementById("conversationTitle").innerHTML = "
    You Won!
    "; + battleships_setup() + battleships_show_turn() + } else if (state === "LOST") { + document.getElementById("conversationTitle").innerHTML = "
    You Lost!
    "; + battleships_setup() + battleships_show_turn() + } else if (state === "RUNNING") { + battleships_setup() + battleships_show_turn() + } + + console.log("BSH chunking ships now ...", JSON.stringify(ships)) + var shipPositions = splitIntoChunks(ships, 3) + console.log("BSH chunky ships: ", JSON.stringify(shipPositions)) + for (var i = 0; i < shipPositions.length; i++) { + console.log("BSH Processing Ship: ", shipPositions[i]) + var ship = shipPositions[i] + var x = parseInt(ship.charAt(0)) + var y = parseInt(ship.charAt(1)) + if (x != -1) { + battleships_ship_positions = battleships_ship_positions + "" + ship + } + var direction = ship.slice(2, 100) + for (var j = 0; j < battleship_ship_lengths[i]; j++) { + var position = y * 10 + x + 1 + if (direction === "U") { + position = position - 10 * j + } else if (direction === "D") { + position = position + 10 * j + } else if (direction === "L") { + position = position - j + } else if (direction === "R") { + position = position + j + } + var field = document.getElementById("battleships:top" + position) + field.className = "ship" + field.innerHTML = "" + field.onclick = null + } + } + console.log("BSH parsing shotsFired ", JSON.stringify(deliv)) + var shots_fired = splitIntoChunks(deliv, 3) + for (var i = 0; i < shots_fired.length; i++) { + var shot = shots_fired[i] + if (shot === "") { + continue + } + var x = parseInt(shot.charAt(0)) + var y = parseInt(shot.charAt(1)) + var outcome = shot.slice(2, 100) + var position = y * 10 + x + 1 + var field = document.getElementById("battleships:bottom" + position) + if (outcome === "M") { + field.className = "miss" + } else if (outcome === "H") { + field.className = "hit" + } else if (outcome === "S") { + field.className = "sunken" + } + field.innerHTML = "" + field.onclick = null + } + console.log("BSH Parsing Shots Received ", JSON.stringify(recv)) + var shots_received = splitIntoChunks(recv, 3) + for (var i = 0; i < shots_received.length; i++) { + var shot = shots_received[i] + if (shot === "") { + continue + } + var x = parseInt(shot.charAt(0)) + var y = parseInt(shot.charAt(1)) + var outcome = shot.slice(2, 100) + var position = y * 10 + x + 1 + var field = document.getElementById("battleships:top" + position) + if (outcome === "M") { + field.className = "miss" + } else if (outcome === "H") { + field.className = "hit" + } else if (outcome === "S") { + field.className = "sunken" + } + field.innerHTML = "" + field.onclick = null + } + if (state === "WAITING") { + battleships_show_turn() + return + } + battleships_show_turn() +} + +/* +* This method parses the ships into readable format. +*/ +function splitIntoChunks(str, chunkSize) { + const chunks = []; + for (let i = 0; i < str.length; i += chunkSize) { + chunks.push(str.substring(i, i + chunkSize)); + } + return chunks; +} + +function reset_battleship_mode() { + battleships_turn = null + battleships_ship_positions = "" + battleship_status = "STOPPED" + battleship_timestamp = "0" + game = "-" + + owner = "-" + peer = "-" +} + + +function show_duels() { + setScenario('duels'); + let c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "
    Battleship Duels
    "; + var container = document.getElementById("duels-container"); + container.innerHTML = ""; + + console.log('show_duels ' + JSON.stringify("gamelist before")); + var gameListString = ""; +// var gameListString = "BSH ownerid1 participantid1 12 STOPPED" + if (window.GamesHandler && typeof window.GamesHandler.createInstanceList === 'function') { + gameListString = window.GamesHandler.createInstanceList(); + console.log('show_duels - gamelist received: ' + gameListString); + } else { + console.error("GamesHandler.createInstanceList is not a function"); + } + + if (gameListString === "") { + console.log('show_duels ' + JSON.stringify("No active duels found.")); + var noDuelDiv = document.createElement("div"); + noDuelDiv.className = "no-duel-box"; + noDuelDiv.innerHTML = "No active duels available..."; + container.appendChild(noDuelDiv); + } else { + var gameList = gameListString.split('$'); + gameList.forEach(function(game) { + var gameParts = game.split(" "); + var gameName = gameParts[0]; + var ownerName = gameParts[1]; + //var ownerAlias = tremola.contacts[owner].alias; + var participantName = gameParts[2]; + //var participantAlias = tremola.contacts[participant].alias; + var startTimeRaw = parseInt(gameParts[3]);; + // Format start time + var date = new Date(startTimeRaw); + var options = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true + }; + var startTime = new Intl.DateTimeFormat('en-US', options).format(date); + var state = gameParts[4]; + console.log('My Id: ' + JSON.stringify(myId)); + var suffix = ".ed25519"; + if (ownerName == myId) { + ownerName = "Me" + participantName = id2b32(participantName); + } else if (participantName == myId) { + participantName = "Me"; + ownerName = id2b32(ownerName); + } else { + participantName = id2b32(participantName); + ownerName = id2b32(ownerName); + } + var turn = gameList[5]; + var ships_rec_delivered = gameList[6]; + + console.log('Game-Container for: ' + JSON.stringify(name)); + + var gameDiv = document.createElement("button"); + gameDiv.className = "duel-button"; + gameDiv.onclick = () => onDuelButtonClicked(game); + + // Change background color based on state + if (state === 'STOPPED') { + gameDiv.classList.add('duel-button-stopped'); + } else if (state === 'INVITED') { + gameDiv.classList.add('duel-button-invited'); + } else if (state === 'RUNNING') { + gameDiv.classList.add('duel-button-running'); + } else if (state === 'WAITING') { + gameDiv.classList.add('duel-button-waiting'); + } else if (state === 'WON') { + gameDiv.classList.add('duel-button-won'); + } else if (state === 'LOST') { + gameDiv.classList.add('duel-button-lost'); + } + // Create Icon for duel + const img = document.createElement("img"); + if (gameName === "BSH") { + img.src = "./games/dpi24-06-battleship/battleship.svg"; + } else { + // other game icons + img.src = "./img/cancel.svg"; + } + img.alt = `Duel Image`; + img.className = "duel-image"; + gameDiv.appendChild(img); + + // Create text for duel button + const span = document.createElement("span"); + span.className = "duel-text"; + span.innerHTML = `Owner: ${ownerName}
    Participant: ${participantName}
    Start Time: ${startTime}
    State: ${state}`; + + gameDiv.appendChild(span); + container.appendChild(gameDiv); + }); + } +} + +/** +* Triggered when you click on an instance in duels overview. +*/ +function onDuelButtonClicked(duelString) { + console.log("Button clicked for: " + JSON.stringify(duelString)); + console.log("myId: ", JSON.stringify(myId)); + var duelList = duelString.split(" "); + game = duelList[0] + console.log("owner: ", JSON.stringify(duelList[1])); + battleship_timestamp = duelList[3] + battleship_status = duelList[4] + switch (battleship_status) { + case "STOPPED": + return; + case "INVITED": + if (duelList[1] != myId) { // check if I am not the owner + // I am not owner + backend("games BSH INVACC " + duelList[1] + " " + myId); // nicht peerId + // TODO possibly add cooldown + } else { + // TODO open game to see ships + owner = duelList[1]; + peer = "-" + battleships(null, duelList[6]); + } + return; + case "WON": // 6 = shotsDeliverOutcome, 7 = shotsReceivedOutcome, 8 = ships + owner = duelList[1]; + peer = duelList[2]; + battleships(null, duelList[6]); + return; + case "LOST": + owner = duelList[1]; + peer = duelList[2]; + battleships(null, duelList[6]); + return; + case "WAITING": + owner = duelList[1]; + peer = duelList[2]; + battleships(false, duelList[6]); + return; + case "RUNNING": + owner = duelList[1]; + peer = duelList[2]; + if (duelList[5] == "0") { + battleships(false, duelList[6]); + } else { + battleships(true, duelList[6]); + } + return; + case "SPEC": + owner = duelList[1]; + peer = duelList[2]; + battleships(null, duelList[6]); + return; + default: + return; + } +} + +// eof diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg new file mode 100644 index 0000000..88109f8 Binary files /dev/null and b/android/tinySSB/app/src/main/assets/web/games/dpi24-06-battleship/placeholder.jpg differ diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css new file mode 100644 index 0000000..30b13aa --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.css @@ -0,0 +1,42 @@ + +/* --------------------------------------------------------------------------- */ +/* Connect 4 */ +/* --------------------------------------------------------------------------- */ + +#connect4-game-board { + height: 40vh; + padding: 10px; + margin-right: 15px; + + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(6, 1fr); + grid-column-gap: 5px; + grid-row-gap: 5px; + + background-color: blue; + border: 2px solid gold; + border-radius: 10px; +} + +.connect4-game_tile { + background-color: white; + border-radius: 50%; + border: 5px solid; +} + +#connect4-game-end-button { + font-size: 1.5em; + padding: 0.5em; + width: 5em; +} + +#connect4-game-leave-button { + font-size: 1.5em; + padding: 0.5em; + width: 5em; +} + +/* eof */ + + diff --git a/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js new file mode 100644 index 0000000..860ec0a --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/games/dpi24-09-connect4/connect4.js @@ -0,0 +1,526 @@ +// games/dpi24-09-connect4/connect4.js + +// Game board size +const CONNECT4_GAME_COLUMNS = 7; +const CONNECT4_GAME_ROWS = 6; + +/** + * Sets up the player selection menu for starting a new game. + * + */ +function connect4_menu_game_players() { + connect4_fill_players(); + prev_scenario = 'connect4-game'; + setScenario("connect4-game-players"); + document.getElementById("div:textarea").style.display = 'none'; + document.getElementById("div:connect4-confirm-player").style.display = 'flex'; + document.getElementById("tremolaTitle").style.display = 'none'; + var c = document.getElementById("conversationTitle"); + c.style.display = null; + c.innerHTML = "Create New Game
    Select contact to play"; + document.getElementById('plus').style.display = 'none'; + closeOverlay(); +} +/** + * Populates the player section menu with a list of available contacts. + * + */ +function connect4_fill_players() { + var choices = ''; + for (var m in tremola.contacts) { + choices += '
    \n'; + } + document.getElementById('lst:connect4-players').innerHTML = choices + document.getElementById(myId).disabled = true; +} +/** + * Sends invitation to the selected user to start a new game. + * + */ +function connect4_send_invite() { + myShortId = id2b32(myId); + var opponent = {} + for (var m in tremola.contacts) { + if (document.getElementById(m).checked) { + opponent = m; + } + } + var opponentShort = id2b32(opponent); // opponent.substring(0,11); + document.getElementById("div:connect4-confirm-player").style.display = 'none'; + setScenario('connect4-game'); // UI change + persist(); + backend(`connect_four_invite ${myShortId} ${opponentShort}`); +} + +/** + * Clear all ongoing games within lst:connect4-games and builds the game button + * for each ongoing game. + */ +function connect4_load_games_list() { + document.getElementById("lst:connect4-games").innerHTML = ''; + for (let gameId in tremola.game_connect4) { + connect4_build_game_item([gameId, tremola.game_connect4[gameId]]); + } +} + +/** + * Create game button to resume ongoing game. + *@param game an Array that contains the GameID and a list with information about the game, in the format: + * [ id, { "alias": "player1 vs player2", "moves": {}, members: [] } ] } + */ +function connect4_build_game_item(game) { // [ id, { "alias": "player1 vs player2", "moves": {}, members: [] } ] } + var row, item = document.createElement('div'), bg; + item.setAttribute('style', 'padding: 0px 5px 10px 5px;'); // old JS (SDK 23) + if (!game[1].members.includes(myShortId)) { + row = ""; + + item.innerHTML = row; + document.getElementById('lst:connect4-games').appendChild(item); + return; + } + + row = ""; + + item.innerHTML = row; + document.getElementById('lst:connect4-games').appendChild(item); +} + +/** + * Is called after a new turn is received via the backend. + * Based on the playerToMove it assigns all board tiles to their + * respective owner and gives over to connect4_populate_game(). + */ +function connect4_game_new_event(e) { + console.log("c4 new event " + JSON.stringify(e)) + //fields + const gameId = e.public[1]; + const playerToMove = e.public[2]; + const members = e.public[3].split(','); + const stonePos = parseInt(e.public[4], 10); + + let board; + //Case if user is not a member of the played game + //User creates new game, such that user has a list of all parallel games, but is not able to open it. + if (!(members.includes(myShortId))) { + if (tremola.game_connect4[gameId] && tremola.game_connect4[gameId].board) { + board = tremola.game_connect4[gameId].board; // Use the existing board, if game already exists in users tremola + } else { + board = Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))); // Initialize a new board + } + tremola.game_connect4[gameId] = { + board: board, + members: members, + currentPlayer: playerToMove, + alias: fid2display(shortToFidMap[members[0]]) + " vs " + fid2display(shortToFidMap[members[1]]), + gameOver: false + }; + connect4_load_games_list(); + return; + } + //Case if User is in members list. + let idlePlayer = members.find(member => member != playerToMove); + + if (tremola.game_connect4[gameId] && tremola.game_connect4[gameId].board) { + board = tremola.game_connect4[gameId].board; // Use the existing board + } else { + board = Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))); // Initialize a new board + } + // Calculating position of the placed stone. + // Board is divided into 42 elements. + // top left of the board starts at 0. Bottom right ends with 41. + // when dividing position int with 7, we get the row. the rest is the column. + if(stonePos != -1) { + const x = stonePos % 7; + const y = Math.floor(stonePos / 7); + tremola.game_connect4[gameId].board[x][y].owner = idlePlayer; + } + + const opponent = playerToMove == myShortId ? idlePlayer : playerToMove; + + tremola.game_connect4[gameId] = { + board: board, + members: members, + currentPlayer: playerToMove, + alias: fid2display(shortToFidMap[opponent]) + " vs " + fid2display(myId), + gameOver: false + }; + persist(); + + connect4_populate_game(gameId); + connect4_load_games_list(); +} + +/** + * This is received when the game is over, either if someone + * won or a player gave up. It updates the up and marks the + * game as over in the store. + * + */ +function connect4_game_end_event(e) { + console.log("c4 gae" + e + ' ' + myShortId) + const gameId = e.public[1]; + const loser = e.public[2]; + const stonePos = e.public[3]; + // Checking if the user ID is in the gameID (Or more concrete: checking if user is a active game member) + //This check needs to be done, such that the user doesn't give up all games or doesn't win if a user from another game gives up + if (gameId.substring(0, 11) != myShortId && gameId.substring(11, 22) != myShortId) { + tremola.game_connect4[gameId].gameOver = true; + delete tremola.game_connect4[gameId]; + connect4_load_games_list(); + return; + } + // Calculation stone position of the last placed winning stone. + if(stonePos != -1) { + const x = stonePos % 7; + const y = Math.floor(stonePos / 7); + // Checking who lost. + if (loser == myShortId) { + if(gameId.substring(0,11) == myShortId) { + tremola.game_connect4[gameId].board[x][y].owner = gameId.substring(11, 22); + } else { + tremola.game_connect4[gameId].board[x][y].owner = gameId.substring(0, 11); + } + } else { + tremola.game_connect4[gameId].board[x][y].owner = myShortId; + } + connect4_populate_game(gameId); + connect4_load_games_list(); + } + + tremola.game_connect4[gameId].gameOver = true; + persist(); + + if (loser != myShortId) { + document.getElementById("connect4-game-turn-indicator").innerHTML = "You WON!"; + } else { + document.getElementById("connect4-game-turn-indicator").innerHTML = "You LOST!"; + } + + document.getElementById("connect4-game-end-button").style.display = `none`; + setTimeout(showEndButton, 4000); + connect4_load_games_list(); +} + +/** + * Sets game-session scenario, title and button. + * Gives over to connect4_populate_game() afterwards. + */ +function connect4_open_game_session(gameId) { + setScenario('connect4-game-session'); + + document.getElementById("connect4-game-session-title").innerHTML = tremola.game_connect4[gameId].alias; + document.getElementById("connect4-game-end-button").onclick = () => connect4_end_game(gameId); + document.getElementById("connect4-game-leave-button").onclick = () => connect4_leave_game(gameId); + connect4_set_turn_indicator(gameId); + + connect4_populate_game(gameId); +} + +/** + * Creates the board and all the clickable tile elements. + * Color of tiles is given based on owner of the tile. + * Can also be used when game is not shown currently. + */ +function connect4_populate_game(gameId) { + document.getElementById('connect4-game-board').innerHTML = ''; + + const { board, members } = tremola.game_connect4[gameId]; + const opponent = members.find(member => member != myShortId); + + for (let y = 0; y < CONNECT4_GAME_ROWS; y++) { + for (let x = 0; x < CONNECT4_GAME_COLUMNS; x++) { + let tile = document.createElement('div'); + tile.className = 'connect4-game_tile'; + tile.onclick = () => connect4_add_stone(gameId, x); + + const { owner } = board[x][y]; + if (owner == myShortId) { + tile.style.backgroundColor = "yellow"; + } else if (owner != myShortId && owner != null) { + tile.style.backgroundColor = "red"; + } + + document.getElementById('connect4-game-board').appendChild(tile); + board[x][y].tile = tile; + + } + } + + connect4_set_turn_indicator(gameId); +} + +/** + * Tries to add a playing stone to the game field. + * After the stone is placed, checks if game is over and if so, + * informs the backend. If not gives over to connect4_end_turn(). + */ +function connect4_add_stone(gameId, column) { + const { board, currentPlayer, members, gameOver } = tremola.game_connect4[gameId]; + // Checking if user is in received game. if not, then user ignores game update message. + if (!(members.includes(myShortId))) { + return; + } + // Checking if it is users turn. else user can't place a stone, since it is not the users turn. + if (currentPlayer != myShortId || gameOver) { + return; + } + // Placing the stone. FreeSlots is the number of free spaces in the collumn. So as long as there are free spaces, + // the user can place a stone. Then the FreeSlots gets decremented, such that the stone is in the lowest possible position. + const freeSlots = board[column].filter(t => t.owner == null).length; + if (freeSlots > 0) { + const boardElement = board[column][freeSlots - 1]; + // Calculating stone position as an integer between 0-41. + const stonePos = ((freeSlots - 1) * 7) + column + boardElement.owner = myShortId; + boardElement.tile.style.backgroundColor = "yellow"; + + const gameover = connect4_check_gameover(gameId); + // Checking if game is over. + if (gameover) { + const loser = members.find(member => member != myShortId); + tremola.game_connect4[gameId].gameOver = true; + persist(); + backend(`connect_four_end ${gameId} ${loser} ${stonePos}`); + return; + } + + connect4_end_turn(gameId, stonePos); + } +} + +/** + * Checks if game is over by checking if stones align so + * that 4 stones are adjacent to each other. + * @param gameId Id of the game that is responded + */ +function connect4_check_gameover(gameId) { + const { board: b } = tremola.game_connect4[gameId]; + + // Check down + for (let y = 0; y < 3; y++) + for (let x = 0; x < 7; x++) + if (connect4_check_line(b[x][y], b[x][y+1], b[x][y+2], b[x][y+3])) + return true; + + // Check right + for (let y = 0; y < 6; y++) + for (let x = 0; x < 4; x++) + if (connect4_check_line(b[x][y], b[x+1][y], b[x+2][y], b[x+3][y])) + return true; + + // Check down-right + for (let y = 0; y < 3; y++) + for (let x = 0; x < 4; x++) + if (connect4_check_line(b[x][y], b[x+1][y+1], b[x+2][y+2], b[x+3][y+3])) + return true; + + // Check down-left + for (let y = 3; y < 6; y++) + for (let x = 0; x < 4; x++) + if (connect4_check_line(b[x][y], b[x+1][y-1], b[x+2][y-2], b[x+3][y-3])) + return true; + + return false; +} + +/** + * Helper function for connect4_check_gameover() to check if 4 stones + * are adjacent. + * @param a first stone to be checked + * @param b second stone to be checked + * @param c third stone to be checked + * @param d fourth stone to be checked + */ +function connect4_check_line(a, b, c, d) { + // Check first cell non-zero and all cells match + return a.owner != null && + a.owner == b.owner && + a.owner == c.owner && + a.owner == d.owner; +} + +/** + * Sets the new currentPlayer to the store and updates + * the UI accordingly with the turn indicator. + * Sends board information after turn is over via backend. + * @param gameId ID of the responsable game + * @param stonePos position of the stone on the board + */ +function connect4_end_turn(gameId, stonePos) { + const { currentPlayer } = tremola.game_connect4[gameId]; + const opponent = tremola.game_connect4[gameId].members.find(member => member != myShortId); + + if (currentPlayer == myShortId) { + tremola.game_connect4[gameId].currentPlayer = opponent; + } else { + tremola.game_connect4[gameId].currentPlayer = myShortId; + } + persist(); + connect4_set_turn_indicator(gameId); + connect4_send_board(gameId, stonePos); +} + +/** + * Sends the position of the new positioned Stone on the board via backend + * @param gameId ID of the responsable game + * @param stonePos Position of the stone that was placed + */ +function connect4_send_board(gameId, stonePos) { + const { board, currentPlayer: playerToMove } = tremola.game_connect4[gameId]; + + const { members } = tremola.game_connect4[gameId]; + + backend(`connect_four ${gameId} ${playerToMove} ${members.join(',')} ${stonePos}`); +} + +/** + * Sets UI turn indicator according to currentPlayer. + * @param gameId ID of the responsable game + */ +function connect4_set_turn_indicator(gameId) { + if (tremola.game_connect4[gameId].currentPlayer == myShortId) { + document.getElementById("connect4-game-turn-indicator").innerHTML = "Your turn!"; + } else { + document.getElementById("connect4-game-turn-indicator").innerHTML = "Wait for your opponent."; + } +} + +/** + * Ends game, either by Giving up or if game is over. After 4 seconds it shows a button to leave the session + * @param gameId ID of the responsable game + */ +function connect4_end_game(gameId) { + document.getElementById("connect4-game-end-button").innerHTML = "Give up"; + document.getElementById("connect4-game-turn-indicator").innerHTML = "You LOST!"; + document.getElementById("connect4-game-end-button").style.display = `none`; + backend(`connect_four_end ${gameId} ${myShortId} ${-1}`); + //Adds a delay for the button to be showed to ensure proper data transmission without errors + setTimeout(showEndButton, 4000); +} + +/** + * Leaves the game after it is finished, via button click, and also deletes it from the database + * @param gameId ID of the Game that is leaved + */ +function connect4_leave_game(gameId) { + document.getElementById("connect4-game-end-button").style.display = `block`; + document.getElementById("connect4-game-leave-button").style.display = `none`; + setScenario("connect4-game"); + persist(); + delete tremola.game_connect4[gameId]; + connect4_load_games_list(); +} + +/** + * Receives an invitation message and if this invite was meant for the player then it shows the invite PupUp, else it is ignored + * @param e invitation event + */ +function connect4_recv_invite(e) { + console.log("c4 rxi " + e + ' ' + myShortId) + const inviterShort = e.public[1]; + const invitedShort = e.public[2]; + + if (myShortId == invitedShort) { + showInvitePopup(inviterShort); + } + +} + +/** + * Shows the end Button when a game is Finished + */ +function showEndButton() { + document.getElementById('connect4-game-leave-button').style.display = 'block'; +} + +/** + * Shows an invitation notification in form of a popUp, whenever a user gets invited to a game by another user. + * This Popup contains of a decline and accept button + * @param inviterShort The shorted ID of the inviter + */ +function showInvitePopup(inviterShort) { + document.getElementById('connect4-game-invite-popup').style.display = 'block'; + inviterLong = shortToFidMap[inviterShort]; + inviterAlias = fid2display(inviterLong); + + document.getElementById('connect4-invite-message').innerText = `You have been invited by ${inviterAlias} to play a game of connect four.`; + document.getElementById('connect4-game-invite-popup').dataset.inviterShort = inviterShort; +} + +/** + * Hides the invite popup when a user declines or accpts it. + */ +function hideInvitePopup() { + document.getElementById('connect4-game-invite-popup').style.display = 'none'; +} + +/** + * Accepts the invite and starts game. It also sends a message that the game is created now and hides the popup after that + */ + +function acceptInvite() { + inviterShort = document.getElementById('connect4-game-invite-popup').dataset.inviterShort; + players = [myShortId, inviterShort]; + gameId = recps2nm(players); + hideInvitePopup(); + + if (tremola.game_connect4 == null) { + tremola.game_connect4 = {}; + } + + if (!(gameId in tremola.game_connect4)) { + tremola.game_connect4[gameId] = { + alias: fid2display(shortToFidMap[inviterShort]) + " vs " + fid2display(myId), + board: Array.from(new Array(7), () => Array.from(new Array(6), () => ({}))), + currentPlayer: inviterShort, + members: players, + gameOver: false + }; + } + + document.getElementById("div:connect4-confirm-player").style.display = 'none'; + connect4_open_game_session(gameId); + + persist(); + connect4_send_board(gameId, -1); +} + +/** + * Declines the invitation and sends the inviter a message that the invitee declined the invite + */ +function declineInvite() { + inviterShort = document.getElementById('connect4-game-invite-popup').dataset.inviterShort; + hideInvitePopup(); + persist(); + backend(`connect_four_decline_invite ${inviterShort} ${myShortId}`) +} + +/** + * Hides the decline information popup when user clicks on the button + */ +function hideDeclinePopup() { + document.getElementById("connect4-game-decline-invite-popup").style.display = 'none'; +} + +/** + * Declines the invitation and sents the inviter an information message about the decline + * @param e buttonclick event + */ +function connect4_invite_declined(e) { + const inviterShort = e.public[1]; + const invitedShort = e.public[2]; + + const invitedAlias = fid2display(shortToFidMap[invitedShort]); + if(myShortId == inviterShort) { + document.getElementById("connect4-game-decline-invite-popup").style.display = 'block'; + document.getElementById('connect4-decline-invite-message').innerText = `Your invitation to ${invitedAlias} has been declined.`; + } +} diff --git a/android/tinySSB/app/src/main/assets/web/games/games.js b/android/tinySSB/app/src/main/assets/web/games/games.js index 976c95d..06fccbe 100644 --- a/android/tinySSB/app/src/main/assets/web/games/games.js +++ b/android/tinySSB/app/src/main/assets/web/games/games.js @@ -4,28 +4,32 @@ function load_game_list() { document.getElementById("lst:games").innerHTML = ''; - load_game_item('Battleship (dpi24.06)', 'games/dpi24-06-battelship/battleship.svg', + load_game_item('Battleship (dpi24.06)', 'games/dpi24-06-battleship/battleship.svg', 'show_duels()', + "Strategy game for two, played on ruled grids on which each player's fleet of warships (randomly generated) are positioned.
    Authors: Mirco Franco and Lars Schneider"); + /* excluded because chrome emulator-only + load_game_item('Snake (dpi24.07)', 'games/dpi24-07-snake/snake.png', '', 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs'); - load_game_item('Snake (dpi24.07)', 'games/dpi24-07-snake/snake.png', + */ + /* excluded because it used the old tremola code based instead of tinySSB + load_game_item('Checkers (dpi24.08)', 'games/dpi24-08-checkers/checkers.svg', '', 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs'); - load_game_item('Checkers (dpi24.08)', 'games/dpi24-08-checkers/checkers.svg', + */ + load_game_item('Connect4 (dpi24.09)', 'games/dpi24-09-connect4/connect4.png', 'setScenario("connect4-game")', 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs'); - load_game_item('Connect4 (dpi24.09)', 'games/dpi24-09-connect4/connect4.png', - 'text text text h aghjwd gldfhjs hlgsf hgljksf hgls fdhglf sdhgl hfgskj hls dfhgjl shgjkls hgl sfdhgjk sdfjklg hljs hfgl dfjlsfs'); - load_game_item('Blackjack (dpi24.10)', 'games/dpi24-10-blackjack/coins.svg', + load_game_item('Blackjack (dpi24.10)', 'games/dpi24-10-blackjack/coins.svg', '', 'crypto tokens based on CRDTs, no mining needed. Ideal for fidelity cards, bartering, recognition tokens in open SW communities, and more.'); - load_game_item('Hangman (dpi24.11)', 'games/dpi24-11-hangman/hangman.svg', + load_game_item('Hangman (dpi24.11)', 'games/dpi24-11-hangman/hangman.svg', '', 'dah dah dah'); } -function load_game_item(title, imageName, descr) { +function load_game_item(title, imageName, fct, descr) { var row, item = document.createElement('div'), bg; item.setAttribute('style', 'padding: 0px 5px 10px 5px;'); // old JS (SDK 23) bg = ' light'; // c[1].forgotten ? ' gray' : ' light'; row = `"; + row += "" + descr + ""; item.innerHTML = row; document.getElementById('lst:games').appendChild(item); } diff --git a/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css b/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css new file mode 100644 index 0000000..1aa8619 --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/prod/dpi24-14-sched/scheduling.css @@ -0,0 +1,88 @@ +/* + prod/dpi24-14-sched/scheduling.css +*/ + + +/* scheduling */ + +.attendance-button { + padding: 5px 10px; + margin-right: 10px; + border: none; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +.attendance-button.attending { + background-color: #e0ffe0; + color: #006400; +} + +.attendance-button.not-attending { + background-color: #ffe0e0; + color: #8b0000; +} + +.attendance-button.active { + font-weight: bold; +} + +.attendance-list { + font-size: 0.9em; + margin-top: 10px; +} + +.appointment-item { + min-height: 150px; /* Increase the minimum height */ + margin-bottom: 15px; /* Add some space between appointments */ +} + +.appointment-item .chat_item_button { + height: 100%; /* Make the button fill the entire height of the item */ + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.appointment-item .attendance-buttons { + display: flex; + justify-content: space-around; + margin: 10px 0; +} + +.appointment-item .attendance-list { + max-height: 100px; /* Limit the height of the attendance list */ + overflow-y: auto; /* Add scrolling if the list is too long */ +} + +.delete-button { + background-color: #ff6b6b; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; +} + +.delete-button:hover { + background-color: #ff4757; +} + +.delete-button { + background-color: #ff6b6b; + color: white; + border: none; + padding: 5px 10px; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + margin-top: 10px; +} + +.delete-button:hover { + background-color: #ff4757; +} + +/* eof */ diff --git a/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css b/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css new file mode 100644 index 0000000..8c63f57 --- /dev/null +++ b/android/tinySSB/app/src/main/assets/web/prod/kanban/kanban.css @@ -0,0 +1,200 @@ +/* + prod/kanban/kanban.css +*/ + +/* --------------------------------------------------------------------------- */ +/* Kanban Board */ +/* --------------------------------------------------------------------------- */ + +.board_item_button { + border: none; + text-align: left; + vertical-align: top; + height: 3em; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + /*background-color: #c1e1c1;*/ +} + +.columns_container { + position: relative; + width: 100%; + /*height: 100%;*/ + display: flex; + flex-direction: row; + justify-content: flex-start; + gap: 10px; +} + +.column { + display: flex; + justify-content: flex-start; + flex-direction: column; + border-radius: 5pt; +} + +.column_wrapper{ + flex: 0 0 36vw; + width: 36vw; +} + +.column_content{ + display: flex; + justify-content: flex-start; + flex-direction: column; + gap: 10px; + padding-bottom: 10px; + padding-top: 10px; +} + +.column_hdr { + width: 100%; + margin: 0 auto; + padding-top:5px; + padding-bottom:5px; + overflow-wrap: break-word; + background-color: #c1e1c1; + border-bottom-color: gray; + border-bottom-style: solid; + border-bottom-width: 2px; + border-radius: 5pt; +} + +/* +.column_options{ + display: flex; + flex-direction: row; + justify-content: space-between; +} +*/ + +.context_options_btn { + border: none; + text-align: left; + height: 3em; + width: 100%; + font-size: medium; + background-color: white; +} + + +.context_menu { + display: none; + width: 10%; + max-height: 80vh; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1001; + overflow-x: hidden; + overflow-y:scroll; +} + +.column_item { + width:95%; + margin: auto; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgb(0 0 0 / 70%); + min-height: 3em; +} + +.item_button { + width:100%; + border: none; + text-align: left; + vertical-align: top; + font-size: medium; + border-radius: 4pt; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + white-space: normal; + word-wrap: break-word; + min-height: 3em; +} + +.item_menu_content { + padding-top: 10px; + float: left; + width: 70%; +} + +.item_menu_desc { + margin-top: 10px; + border: none; + outline: none; + background-color: rgb(211,211,211); +} + +.item_menu_buttons{ + padding-top: 10px; + float: right; + width: 30%; +} + +.item_menu_button { + width: 90%; + float: right; + margin:5px auto; + display:block; +} + +.div:item_menu_assignees_container { + padding-top: 5px; + width: 100%; + display: flex; + justify-content: flex-start; + flex-direction: column; +} + +.column_footer { + width: 100%; + padding-top: 5px; + padding-bottom: 5px; + border-top-color: gray; + border-top-style: solid; + border-top-width: 2px; +} + + +.kanban_invitation_container { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + gap: 0px 0px; + grid-template-areas: + "text btns"; + width:100%; + box-shadow: 0 0 5px rgba(0,0,0,0.7); + border-radius: 4pt; + height: 3em; + margin-top: 5px; +} + +.kanban_invitation_text_container { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + gap: 0px 0px; + grid-template-areas: + "name" + "author"; + grid-area: text; +} + +.kanban_create_personal_btn { + background: none; + border: none; + cursor: pointer; + font-size: 16px; + margin: 0 10px; + background-color: #51a4d2; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + height: 40px; + width: 35px +} + +/* eof */ \ No newline at end of file diff --git a/android/tinySSB/app/src/main/assets/web/tremola.css b/android/tinySSB/app/src/main/assets/web/tremola.css index 388c02c..27a6d07 100644 --- a/android/tinySSB/app/src/main/assets/web/tremola.css +++ b/android/tinySSB/app/src/main/assets/web/tremola.css @@ -516,281 +516,4 @@ input:checked + .slider:before { filter: invert(62%) sepia(8%) saturate(116%) hue-rotate(145deg) brightness(89%) contrast(89%); } -/* --------------------------------------------------------------------------- */ -/* Kanban Board */ -/* --------------------------------------------------------------------------- */ - -.board_item_button { - border: none; - text-align: left; - vertical-align: top; - height: 3em; - font-size: medium; - border-radius: 4pt; - box-shadow: 0 0 5px rgba(0,0,0,0.7); - /*background-color: #c1e1c1;*/ -} - -.columns_container { - position: relative; - width: 100%; - /*height: 100%;*/ - display: flex; - flex-direction: row; - justify-content: flex-start; - gap: 10px; -} - -.column { - display: flex; - justify-content: flex-start; - flex-direction: column; - border-radius: 5pt; -} - -.column_wrapper{ - flex: 0 0 36vw; - width: 36vw; -} - -.column_content{ - display: flex; - justify-content: flex-start; - flex-direction: column; - gap: 10px; - padding-bottom: 10px; - padding-top: 10px; -} - -.column_hdr { - width: 100%; - margin: 0 auto; - padding-top:5px; - padding-bottom:5px; - overflow-wrap: break-word; - background-color: #c1e1c1; - border-bottom-color: gray; - border-bottom-style: solid; - border-bottom-width: 2px; - border-radius: 5pt; -} - -/* -.column_options{ - display: flex; - flex-direction: row; - justify-content: space-between; -} -*/ - -.context_options_btn { - border: none; - text-align: left; - height: 3em; - width: 100%; - font-size: medium; - background-color: white; -} - - -.context_menu { - display: none; - width: 10%; - max-height: 80vh; - position: absolute; - background-color: #f9f9f9; - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1001; - overflow-x: hidden; - overflow-y:scroll; -} - -.column_item { - width:95%; - margin: auto; - font-size: medium; - border-radius: 4pt; - box-shadow: 0 0 5px rgb(0 0 0 / 70%); - min-height: 3em; -} - -.item_button { - width:100%; - border: none; - text-align: left; - vertical-align: top; - font-size: medium; - border-radius: 4pt; - box-shadow: 0 0 5px rgba(0,0,0,0.7); - white-space: normal; - word-wrap: break-word; - min-height: 3em; -} - -.item_menu_content { - padding-top: 10px; - float: left; - width: 70%; -} - -.item_menu_desc { - margin-top: 10px; - border: none; - outline: none; - background-color: rgb(211,211,211); -} - -.item_menu_buttons{ - padding-top: 10px; - float: right; - width: 30%; -} - -.item_menu_button { - width: 90%; - float: right; - margin:5px auto; - display:block; -} - -.div:item_menu_assignees_container { - padding-top: 5px; - width: 100%; - display: flex; - justify-content: flex-start; - flex-direction: column; -} - -.column_footer { - width: 100%; - padding-top: 5px; - padding-bottom: 5px; - border-top-color: gray; - border-top-style: solid; - border-top-width: 2px; -} - - -.kanban_invitation_container { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr; - gap: 0px 0px; - grid-template-areas: - "text btns"; - width:100%; - box-shadow: 0 0 5px rgba(0,0,0,0.7); - border-radius: 4pt; - height: 3em; - margin-top: 5px; -} - -.kanban_invitation_text_container { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; - gap: 0px 0px; - grid-template-areas: - "name" - "author"; - grid-area: text; -} - -.kanban_create_personal_btn { - background: none; - border: none; - cursor: pointer; - font-size: 16px; - margin: 0 10px; - background-color: #51a4d2; - background-size: contain; - background-repeat: no-repeat; - background-position: center; - height: 40px; - width: 35px -} - -/* scheduling */ - -.attendance-button { - padding: 5px 10px; - margin-right: 10px; - border: none; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.attendance-button.attending { - background-color: #e0ffe0; - color: #006400; -} - -.attendance-button.not-attending { - background-color: #ffe0e0; - color: #8b0000; -} - -.attendance-button.active { - font-weight: bold; -} - -.attendance-list { - font-size: 0.9em; - margin-top: 10px; -} - -.appointment-item { - min-height: 150px; /* Increase the minimum height */ - margin-bottom: 15px; /* Add some space between appointments */ -} - -.appointment-item .chat_item_button { - height: 100%; /* Make the button fill the entire height of the item */ - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.appointment-item .attendance-buttons { - display: flex; - justify-content: space-around; - margin: 10px 0; -} - -.appointment-item .attendance-list { - max-height: 100px; /* Limit the height of the attendance list */ - overflow-y: auto; /* Add scrolling if the list is too long */ -} - -.delete-button { - background-color: #ff6b6b; - color: white; - border: none; - padding: 5px 10px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.delete-button:hover { - background-color: #ff4757; -} - -.delete-button { - background-color: #ff6b6b; - color: white; - border: none; - padding: 5px 10px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; - margin-top: 10px; -} - -.delete-button:hover { - background-color: #ff4757; -} - /* eof */ diff --git a/android/tinySSB/app/src/main/assets/web/tremola.html b/android/tinySSB/app/src/main/assets/web/tremola.html index f148823..81942b6 100644 --- a/android/tinySSB/app/src/main/assets/web/tremola.html +++ b/android/tinySSB/app/src/main/assets/web/tremola.html @@ -3,6 +3,10 @@ + + + + @@ -17,6 +21,8 @@ + + @@ -132,6 +138,43 @@ + + +
    @@ -325,6 +368,42 @@
    + + + + + + +
    did
    @@ -624,6 +703,10 @@
    + +