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 @@
+
+
+
+
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