diff --git a/assets/mines.png b/assets/mines.png index bb9e45f..bd8c86d 100644 Binary files a/assets/mines.png and b/assets/mines.png differ diff --git a/assets/time.svg b/assets/time.svg new file mode 100644 index 0000000..69994af --- /dev/null +++ b/assets/time.svg @@ -0,0 +1,59 @@ + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/po/mines.neothethird.pot b/po/mines.neothethird.pot index e67396b..7a54d59 100644 --- a/po/mines.neothethird.pot +++ b/po/mines.neothethird.pot @@ -8,6 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: mines.neothethird\n" "Report-Msgid-Bugs-To: \n" + "POT-Creation-Date: 2018-09-23 21:06+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" @@ -18,76 +19,96 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #. TRANSLATORS: Title for dialog shown when starting a new game while one in progress -#: ../qml/Main.qml:92 +#: ../qml/Main.qml:138 msgid "Game in progress" msgstr "" #. TRANSLATORS: Content for dialog shown when starting a new game while one in progress -#: ../qml/Main.qml:94 +#: ../qml/Main.qml:140 msgid "Are you sure you want to restart this game?" msgstr "" #. TRANSLATORS: Button in new game dialog that cancels the current game and starts a new one -#: ../qml/Main.qml:97 +#: ../qml/Main.qml:143 msgid "Restart game" msgstr "" #. TRANSLATORS: Button in new game dialog that cancels new game request -#: ../qml/Main.qml:106 +#: ../qml/Main.qml:152 msgid "Continue current game" msgstr "" #. TRANSLATORS: Title for dialog confirming if scores should be cleared #. TRANSLATORS: Button in clear scores dialog that clears scores #. TRANSLATORS: Action in high scores page that clears scores -#: ../qml/Main.qml:117 ../qml/Main.qml:122 ../qml/Main.qml:324 +#: ../qml/Main.qml:163 ../qml/Main.qml:168 ../qml/Main.qml:505 msgid "Clear scores" msgstr "" #. TRANSLATORS: Content for dialog confirming if scores should be cleared -#: ../qml/Main.qml:119 +#: ../qml/Main.qml:165 msgid "Existing scores will be deleted. This cannot be undone." msgstr "" #. TRANSLATORS: Button in clear scores dialog that cancels clear scores request -#: ../qml/Main.qml:131 +#: ../qml/Main.qml:177 msgid "Keep existing scores" msgstr "" +#. TRANSLATORS: Title for dialog shown when starting a new game while one in progress +#: ../qml/Main.qml:188 +msgid "Restar needed to apply the new theme" +msgstr "" + +#. TRANSLATORS: Content for dialog shown when starting a new game while one in progress +#: ../qml/Main.qml:190 +msgid "Quit now?" +msgstr "" + +#. TRANSLATORS: Button in new game dialog that cancels the current game and starts a new one +#: ../qml/Main.qml:193 +msgid "Yes" +msgstr "" + +#. TRANSLATORS: Button in new game dialog that cancels new game request +#: ../qml/Main.qml:202 +msgid "No" +msgstr "" + #. TRANSLATORS: Title of application -#: ../qml/Main.qml:147 ../qml/Main.qml:234 +#: ../qml/Main.qml:218 ../qml/Main.qml:412 msgid "Mines" msgstr "" #. TRANSLATORS: Action on main page that shows game instructions #. TRANSLATORS: Title of page with game instructions -#: ../qml/Main.qml:151 ../qml/Main.qml:208 +#: ../qml/Main.qml:225 ../qml/Main.qml:386 msgid "How to Play" msgstr "" #. TRANSLATORS: Action on main page that shows settings dialog #. TRANSLATORS: Title of page showing settings -#: ../qml/Main.qml:157 ../qml/Main.qml:337 +#: ../qml/Main.qml:231 ../qml/Main.qml:521 msgid "Settings" msgstr "" #. TRANSLATORS: Action on main page that starts a new game -#: ../qml/Main.qml:163 +#: ../qml/Main.qml:237 msgid "New Game" msgstr "" -#: ../qml/Main.qml:252 +#: ../qml/Main.qml:430 msgid "Version: " msgstr "" #. TRANSLATORS: Short description -#: ../qml/Main.qml:261 +#: ../qml/Main.qml:439 msgid "" "Mines is a puzzle game where the goal is to find the mines in a minefield." msgstr "" #. TRANSLATORS: Game instructions -#: ../qml/Main.qml:271 +#: ../qml/Main.qml:449 msgid "" "The minefield is divided into a grid of squares. Touch a square to check if " "there is a mine there. If no mine is present the square will show the number " @@ -97,7 +118,7 @@ msgid "" msgstr "" #. TRANSLATORS: GPL notice -#: ../qml/Main.qml:281 +#: ../qml/Main.qml:459 msgid "" "This program is free software: you can redistribute it and/or modify it " "under the terms of the GNU General Public License as published by the Free " @@ -109,52 +130,77 @@ msgid "" "License for more details." msgstr "" -#: ../qml/Main.qml:290 +#: ../qml/Main.qml:468 msgid "SOURCE" msgstr "" -#: ../qml/Main.qml:290 +#: ../qml/Main.qml:468 msgid "ISSUES" msgstr "" -#: ../qml/Main.qml:290 +#: ../qml/Main.qml:468 msgid "DONATE" msgstr "" -#: ../qml/Main.qml:300 +#: ../qml/Main.qml:478 msgid "Copyright (c) 2015 Robert Ancell" msgstr "" -#: ../qml/Main.qml:309 +#: ../qml/Main.qml:487 msgid "Copyright (c) 2017 Jan Sprinz " msgstr "" #. TRANSLATORS: Title of page showing high scores -#: ../qml/Main.qml:321 +#: ../qml/Main.qml:502 msgid "High Scores" msgstr "" #. TRANSLATORS: Label beside checkbox setting for controlling vibrations when placing flags -#: ../qml/Main.qml:349 +#: ../qml/Main.qml:532 msgid "Vibrate when placing flags" msgstr "" #. TRANSLATORS: Label above setting to choose the minefield size -#: ../qml/Main.qml:359 +#: ../qml/Main.qml:542 msgid "Minefield size:" msgstr "" #. TRANSLATORS: Setting name for small minefield -#: ../qml/Main.qml:366 +#: ../qml/Main.qml:549 msgid "Small" msgstr "" +#. TRANSLATORS: Setting name for medium minefield +#: ../qml/Main.qml:552 +msgid "Medium" +msgstr "" + #. TRANSLATORS: Setting name for large minefield -#: ../qml/Main.qml:369 +#: ../qml/Main.qml:555 msgid "Large" msgstr "" #. TRANSLATORS: Description format for minefield size, %width%, %height% and %nmines% is replaced with the field width, height and number of mines -#: ../qml/Main.qml:375 +#: ../qml/Main.qml:561 msgid "%width%×%height%, %nmines% mines" msgstr "" + +#. TRANSLATORS: Label above setting to choose the minefield size +#: ../qml/Main.qml:572 +msgid "Theme:" +msgstr "" + +#. TRANSLATORS: Setting name for small minefield +#: ../qml/Main.qml:579 +msgid "Classic" +msgstr "" + +#. TRANSLATORS: Setting name for medium minefield +#: ../qml/Main.qml:582 +msgid "Dark" +msgstr "" + +#. TRANSLATORS: Setting name for large minefield +#: ../qml/Main.qml:585 +msgid "Ubuntu" +msgstr "" diff --git a/qml/Main.qml b/qml/Main.qml index a752179..7d1d4e2 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -38,6 +38,16 @@ MainView { property string version: "1.1" + property var grid_width: 8 + property var grid_height: 8 + property var n_mines: 10 + + property var background_color: "#ffffff" + property var unchecked_color: "#AAAAFF" + property var checked_color: "#CCCCFF" + property var font_color: "#000000" + property var theme_counter: 0 + width: units.gu(40) height: units.gu(71) @@ -52,7 +62,7 @@ MainView { Component.onCompleted: { get_state_database().transaction(function(t) { try { - var r = t.executeSql('SELECT use_haptic, grid_width, grid_height, n_mines FROM Settings') + var r = t.executeSql('SELECT use_haptic, grid_width, grid_height, n_mines, background_color, unchecked_color, checked_color, font_color FROM Settings') var item = r.rows.item(0) haptic_check.checked = item.use_haptic for(var i = 0; i < size_selector.model.count; i++) { @@ -62,6 +72,21 @@ MainView { break } } + for(var i = 0; i < theme_selector.model.count; i++) { + var s = theme_selector.model.get(i) + if(s.background_color == item.background_color && s.unchecked_color == item.unchecked_color && s.checked_color == item.checked_color && s.font_color == item.font_color) { + theme_selector.selectedIndex = i + if (i == 0){theme_counter++} + break + } + } + grid_height = item.grid_height + grid_width = item.grid_width + n_mines = item.n_mines + background_color = item.background_color + unchecked_color = item.unchecked_color + checked_color = item.checked_color + font_color = item.font_color } catch(e) { } @@ -72,9 +97,20 @@ MainView { function save_state() { get_state_database().transaction(function(t) { var grid_options = size_selector.model.get(size_selector.selectedIndex) + var theme_options = theme_selector.model.get(theme_selector.selectedIndex) // The lock field is to ensure the INSERT will always replace this row instead of adding another - t.executeSql("CREATE TABLE IF NOT EXISTS Settings(lock INTEGER, use_haptic BOOLEAN, grid_width INTEGER, grid_height INTEGER, n_mines INTEGER, PRIMARY KEY (lock))") - t.executeSql("INSERT OR REPLACE INTO Settings VALUES(0, ?, ?, ?, ?)", [haptic_check.checked, grid_options.grid_width, grid_options.grid_height, grid_options.n_mines]) + t.executeSql("CREATE TABLE IF NOT EXISTS Settings(lock INTEGER, use_haptic BOOLEAN, grid_width INTEGER, grid_height INTEGER, n_mines INTEGER, background_color STRING, unchecked_color STRING, checked_color STRING, font_color STIRNG, PRIMARY KEY (lock))") + try { + t.executeSql("INSERT OR REPLACE INTO Settings VALUES(0, ?, ?, ?, ?, ?, ?, ?, ?)", [haptic_check.checked, grid_options.grid_width, grid_options.grid_height, grid_options.n_mines, theme_options.background_color, theme_options.unchecked_color, theme_options.checked_color, theme_options.font_color]) + } + catch(e) { + //Need to update database of previous version + t.executeSql("ALTER TABLE Settings ADD background_color STRING") + t.executeSql("ALTER TABLE Settings ADD unchecked_color STRING") + t.executeSql("ALTER TABLE Settings ADD checked_color STRING") + t.executeSql("ALTER TABLE Settings ADD font_color STRING") + t.executeSql("INSERT OR REPLACE INTO Settings VALUES(0, ?, ?, ?, ?, ?, ?, ?, ?)", [haptic_check.checked, grid_options.grid_width, grid_options.grid_height, grid_options.n_mines, theme_options.background_color, theme_options.unchecked_color, theme_options.checked_color, theme_options.font_color]) + } }) } @@ -82,6 +118,17 @@ MainView { { var grid_options = size_selector.model.get(size_selector.selectedIndex) minefield.set_size(grid_options.grid_width, grid_options.grid_height, grid_options.n_mines) + reset_timer() + } + + function reset_timer() + { + if (!minefield.completed && !timer.running) { timer.start() } + } + + Rectangle { + anchors.fill: parent + color: background_color } Component { @@ -134,6 +181,31 @@ MainView { } } + Component { + id: confirm_restart_needed + Dialog { + id: d + // TRANSLATORS: Title for dialog shown when starting a new game while one in progress + title: i18n.tr("Restar needed to apply the new theme") + // TRANSLATORS: Content for dialog shown when starting a new game while one in progress + text: i18n.tr("Quit now?") + Button { + // TRANSLATORS: Button in new game dialog that cancels the current game and starts a new one + text: i18n.tr("Yes") + color: UbuntuColors.red + onClicked: { + Qt.quit() + PopupUtils.close(d) + } + } + Button { + // TRANSLATORS: Button in new game dialog that cancels new game request + text: i18n.tr("No") + onClicked: PopupUtils.close(d) + } + } + } + PageStack { id: page_stack Component.onCompleted: push(main_page) @@ -145,6 +217,9 @@ MainView { id: main_header // TRANSLATORS: Title of application title: i18n.tr("Mines") + StyleHints { + backgroundColor: background_color + } trailingActionBar.actions: [ Action { // TRANSLATORS: Action on main page that shows game instructions @@ -175,11 +250,14 @@ MainView { MinefieldModel { id: minefield onSolved: { - get_history_database().transaction(function(t) { - t.executeSql("CREATE TABLE IF NOT EXISTS History(grid_width INTEGER, grid_height INTEGER, n_mines INTEGER, date TEXT, duration INTEGER)") - var duration = minefield.end_time - minefield.start_time - t.executeSql("INSERT INTO History VALUES(?, ?, ?, ?, ?)", [minefield.columns, minefield.rows, minefield.n_mines, minefield.start_time.toISOString(), duration]) - }) + if (minefield.loosed != true) { + get_history_database().transaction(function(t) { + t.executeSql("CREATE TABLE IF NOT EXISTS History(grid_width INTEGER, grid_height INTEGER, n_mines INTEGER, date TEXT, duration INTEGER)") + var duration = minefield.end_time - minefield.start_time + t.executeSql("INSERT INTO History VALUES(?, ?, ?, ?, ?)", [minefield.columns, minefield.rows, minefield.n_mines, minefield.start_time.toISOString(), duration]) + }) + } + timer.stop() } } @@ -187,7 +265,7 @@ MainView { width: parent.width anchors { top: main_header.bottom - bottom: parent.bottom + bottom: statBar.top } anchors.margins: units.gu(2) MinefieldView { @@ -197,6 +275,104 @@ MainView { use_haptic_feedback: haptic_check.checked } } + + Rectangle { + id: statBar + width: parent.width + height: units.gu(8) + anchors.bottom: parent.bottom + + Rectangle { + height: parent.height + width: parent.width/2 + color: background_color + anchors { + left: parent.left + } + + Row { + height: parent.height + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + Image { + id: time_icon + anchors { + verticalCenter: parent.verticalCenter + margins: units.gu(2) + } + source: "../assets/time.svg" + } + Rectangle{ + width: units.gu(8) + height: parent.height + color: background_color + anchors.verticalCenter: parent.verticalCenter + + Item { + Component.onCompleted: reset_timer() + Timer { + id: timer + interval: 500; running: false; repeat: true + onTriggered: time.text = minefield.update_time_elapsed() + } + } + Text { + id: time + color: font_color + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + } + } + } + } + + Rectangle { + height: parent.height + width: parent.width/2 + color: background_color + anchors { + right: parent.right + } + + Row { + height: parent.height + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + Image { + id: mine_icon + anchors { + verticalCenter: parent.verticalCenter + margins: units.gu(2) + } + source: "../assets/mine.svg" + } + + Rectangle{ + width: units.gu(8) + height: parent.height + color: background_color + anchors.verticalCenter: parent.verticalCenter + + Text { + color: font_color + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + text: minefield.n_flags + "/" + minefield.n_mines + } + } + } + } + } } Page { @@ -204,6 +380,9 @@ MainView { visible: false header : PageHeader { id: how_to_play_header + StyleHints { + backgroundColor: background_color + } // TRANSLATORS: Title of page with game instructions title: i18n.tr("How to Play") flickable: how_to_play_flick @@ -317,6 +496,9 @@ MainView { visible: false header: PageHeader { id: scores_header + StyleHints { + backgroundColor: background_color + } // TRANSLATORS: Title of page showing high scores title: i18n.tr("High Scores") Action { @@ -333,11 +515,13 @@ MainView { visible: false header: PageHeader { id: settings_header + StyleHints { + backgroundColor: background_color + } // TRANSLATORS: Title of page showing settings title: i18n.tr("Settings") } - Column { width: parent.width anchors { @@ -364,6 +548,9 @@ MainView { case "small": // TRANSLATORS: Setting name for small minefield return i18n.tr("Small") + case "medium": + // TRANSLATORS: Setting name for medium minefield + return i18n.tr("Medium") case "large": // TRANSLATORS: Setting name for large minefield return i18n.tr("Large") @@ -380,6 +567,34 @@ MainView { reset_field() } } + ListItem.ItemSelector { + id: theme_selector + // TRANSLATORS: Label above setting to choose the minefield size + text: i18n.tr("Theme:") + model: theme_model + delegate: OptionSelectorDelegate { + text: { + switch(name) { + case "classic": + // TRANSLATORS: Setting name for small minefield + return i18n.tr("Classic") + case "dark": + // TRANSLATORS: Setting name for medium minefield + return i18n.tr("Dark") + case "ubuntu": + // TRANSLATORS: Setting name for large minefield + return i18n.tr("Ubuntu") + default: + return "" + } + } + } + onSelectedIndexChanged: { + save_state() + if(theme_counter > 0) {PopupUtils.open(confirm_restart_needed)} + else {theme_counter++} + } + } } ListModel { @@ -391,11 +606,41 @@ MainView { n_mines: 10 } ListElement { - name: "large" + name: "medium" grid_width: 10 grid_height: 16 n_mines: 25 } + ListElement { + name: "large" + grid_width: 18 + grid_height: 24 + n_mines: 99 + } + } + ListModel { + id: theme_model + ListElement { + name: "classic" + background_color: "#ffffff" + unchecked_color: "#AAAAFF" + checked_color: "#CCCCFF" + font_color: "#000000" + } + ListElement { + name: "dark" + background_color: "#aba79e" + unchecked_color: "#c3beba" + checked_color: "#eee4d8" + font_color: "#000000" + } + ListElement { + name: "ubuntu" + background_color: "#ffffff" + unchecked_color: "#babdb6" + checked_color: "#dededc" + font_color: "#6b6561" + } } } } diff --git a/qml/MinefieldModel.qml b/qml/MinefieldModel.qml index 8502b01..dd0d7a8 100644 --- a/qml/MinefieldModel.qml +++ b/qml/MinefieldModel.qml @@ -33,9 +33,12 @@ ListModel { property bool started: false property int n_checked: 0 property int n_mines + property int n_flags: 0 property bool completed: false + property bool loosed: false property var start_time property var end_time + property double time_elapsed signal solved() function set_size(n_columns, n_rows, n) { @@ -44,7 +47,9 @@ ListModel { started = false n_checked = 0 n_mines = n + n_flags = 0 completed = false + loosed = false clear() for(var row = 0; row < n_rows; row++) { for(var column = 0; column < n_columns; column++) { @@ -71,6 +76,9 @@ ListModel { // Can only flag unknown cells if(cell.checked) { return false } + // Update n_flags + if(cell.flagged) { n_flags-- } + else { n_flags++ } // toggle flag status cell.flagged = !cell.flagged @@ -109,6 +117,8 @@ ListModel { if(cell.has_mine) { completed = true + loosed = true + solved() } else { var n_flagged_mines = n_surrounding_flagged(cell.column, cell.row) if(cell.n_surrounding <= n_flagged_mines) { @@ -179,4 +189,16 @@ ListModel { } } } + + function update_time_elapsed() { + if (started) { + time_elapsed = new Date() - start_time + var minutes = Math.floor(time_elapsed / 60000) + var seconds = ((time_elapsed % 60000) / 1000).toFixed(0) + return (seconds == 60 ? (minutes+1) + ":00" : (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds) + } + else { + return "00:00" + } + } } diff --git a/qml/MinefieldView.qml b/qml/MinefieldView.qml index 32f0e4d..8d51772 100644 --- a/qml/MinefieldView.qml +++ b/qml/MinefieldView.qml @@ -34,6 +34,7 @@ Grid { columns: model.columns rows: model.rows rotation: landscape ? 90 : 0 + spacing: 5 Behavior on rotation { NumberAnimation { easing: UbuntuAnimation.StandardEasing @@ -50,12 +51,11 @@ Grid { id: repeater model: grid.model Rectangle { - width: grid.cell_size - height: grid.cell_size - color: checked ? "#CCCCFF" : "#AAAAFF" - border.width: 1 - border.color: checked ? color: "#4040FF" + width: grid.cell_size - parent.spacing + height: grid.cell_size - parent.spacing + color: checked ? checked_color : unchecked_color rotation: grid.landscape ? -90 : 0 + radius: 5 Image { id: image anchors.centerIn: parent