feat: phase 1 - einstellungen mit lautstärke und spruchlisten

- Settings.qml mit Lautstärke-Slidern für Musik und Knack-Geräusch
- Spruchlisten-Auswahl (classic, farmer_wisdom, unfortune)
- StackLayout für Navigation zwischen Hauptseite und Einstellungen
- Python: load_setting/save_setting für generische Einstellungen
- Python: Volume-Einstellungen und Spruchlisten-Verwaltung
- ASSET_REQUIREMENTS.md für Grafik-Spezifikationen
- JSON-Dateien: classic.json, farmer_wisdom.json, unfortune.json

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
darklithium
2026-06-02 21:26:41 +02:00
parent da719d7670
commit c4e7d4bd55
7 changed files with 1019 additions and 235 deletions
+185 -147
View File
@@ -20,6 +20,10 @@ MainView {
property bool musicPlaying: false
property bool appInitialized: false
// Medien-Player als globale Properties für Zugriff aus Settings
property MediaPlayer globalMediaPlayer: mediaPlayer
property MediaPlayer globalCrackPlayer: crackMediaPlayer
Python {
id: py
Component.onCompleted: {
@@ -44,170 +48,204 @@ MainView {
}
// ====================================================================
// INITIALISIERUNGS-TIMER
// STACK LAYOUT FÜR NAVIGATION
// ====================================================================
// WICHTIG: PyOtherSide braucht Zeit zum Laden!
// 1 Sekunde Verzögerung verhindert Race Conditions
Timer {
id: initTimer
interval: 1000
running: true
repeat: false
onTriggered: {
try {
currentFortune = py.call_sync("fortunecookie.get_initial_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
// Musik-Status laden (Last-State)
musicPlaying = py.call_sync("fortunecookie.get_music_enabled", []);
console.log("DEBUG QML: musicPlaying geladen: " + musicPlaying);
// MediaPlayer Zustand synchronisieren
if (musicPlaying) {
mediaPlayer.play();
}
// UI erst nach Initialisierung anzeigen
appInitialized = true;
} catch (e) {
console.log("ERROR QML: Initialisierung fehlgeschlagen: " + e);
}
}
}
Page {
id: mainPage
StackLayout {
id: mainStack
anchors.fill: parent
currentIndex: 0
header: PageHeader {
title: "Fortune Cookie"
}
// Medien-Player für Settings zugänglich machen
property MediaPlayer stackMediaPlayer: root.globalMediaPlayer
property MediaPlayer stackCrackPlayer: root.globalCrackPlayer
Image {
id: cookieImage
anchors.centerIn: parent
// Geöffneter Keks ist größer als geschlossener
width: fortuneOpened ? units.gu(36) : units.gu(32)
height: fortuneOpened ? units.gu(28) : units.gu(24)
source: fortuneOpened ? Qt.resolvedUrl("../assets/cookie_open2.png") : Qt.resolvedUrl("../assets/cookie_closed2.png")
fillMode: Image.PreserveAspectFit
// ================================================================
// SEITE 0: HAUPTSPIELBILDSCHIRM
// ================================================================
Page {
id: mainPage
objectName: "mainPage"
MouseArea {
anchors.fill: parent
property real startY: 0
header: PageHeader {
title: "Fortune Cookie"
onPressed: startY = mouseY
onReleased: {
if (mouseY < startY - units.gu(2)) {
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
currentFortuneLabel.visible = true;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_open2.png");
});
// Einstellungen-Button im Header
actions: [
Action {
iconName: "settings"
text: "Einstellungen"
onTriggered: {
mainStack.currentIndex = 1;
}
}
}
]
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!fortuneOpened) {
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
currentFortuneLabel.visible = true;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_open2.png");
});
} else {
fortuneOpened = false;
currentFortuneLabel.text = "";
currentFortuneLabel.visible = false;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
}
}
}
}
Label {
id: currentFortuneLabel
anchors {
top: cookieImage.bottom
topMargin: units.gu(2)
left: parent.left
right: parent.right
leftMargin: units.gu(2)
rightMargin: units.gu(2)
}
text: ""
fontSize: "large"
horizontalAlignment: Text.AlignHCenter
visible: false
wrapMode: Text.WordWrap
MouseArea {
anchors.fill: parent
onClicked: {
py.call("fortunecookie.get_new_fortune", [], function() {
fortuneOpened = false;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
// ============================================================
// INITIALISIERUNGS-TIMER
// ============================================================
Timer {
id: initTimer
interval: 1000
running: true
repeat: false
onTriggered: {
try {
currentFortune = py.call_sync("fortunecookie.get_initial_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
});
// Musik-Status laden
musicPlaying = py.call_sync("fortunecookie.get_music_enabled", []);
console.log("DEBUG QML: musicPlaying geladen: " + musicPlaying);
// MediaPlayer Zustand synchronisieren
if (musicPlaying) {
mediaPlayer.play();
}
// Volumes laden und setzen
var musicVol = py.call_sync("fortunecookie.get_music_volume", []);
var crackVol = py.call_sync("fortunecookie.get_crack_volume", []);
mediaPlayer.volume = musicVol;
crackMediaPlayer.volume = crackVol;
appInitialized = true;
} catch (e) {
console.log("ERROR QML: Initialisierung fehlgeschlagen: " + e);
}
}
}
Image {
id: cookieImage
anchors.centerIn: parent
width: fortuneOpened ? units.gu(36) : units.gu(32)
height: fortuneOpened ? units.gu(28) : units.gu(24)
source: fortuneOpened ? Qt.resolvedUrl("../assets/cookie_open2.png") : Qt.resolvedUrl("../assets/cookie_closed2.png")
fillMode: Image.PreserveAspectFit
MouseArea {
anchors.fill: parent
property real startY: 0
onPressed: startY = mouseY
onReleased: {
if (mouseY < startY - units.gu(2)) {
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
currentFortuneLabel.visible = true;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_open2.png");
});
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!fortuneOpened) {
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
currentFortuneLabel.visible = true;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_open2.png");
});
} else {
fortuneOpened = false;
currentFortuneLabel.text = "";
currentFortuneLabel.visible = false;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
}
}
}
}
Label {
id: currentFortuneLabel
anchors {
top: cookieImage.bottom
topMargin: units.gu(2)
left: parent.left
right: parent.right
leftMargin: units.gu(2)
rightMargin: units.gu(2)
}
text: ""
fontSize: "large"
horizontalAlignment: Text.AlignHCenter
visible: false
wrapMode: Text.WordWrap
MouseArea {
anchors.fill: parent
onClicked: {
py.call("fortunecookie.get_new_fortune", [], function() {
fortuneOpened = false;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
});
}
}
}
// ================================================================
// MUSIK-BUTTON
// ================================================================
Label {
id: musicButton
anchors {
right: parent.right
bottom: parent.bottom
margins: units.gu(2)
}
width: units.gu(10)
height: units.gu(10)
text: musicPlaying ? "\uD83D\uDD0A" : "\uD83D\uDD07"
fontSize: "xx-large"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: theme.palette.normalText
visible: appInitialized
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("DEBUG QML: Music button clicked, current musicPlaying: " + musicPlaying);
if (musicPlaying) {
mediaPlayer.stop();
} else {
mediaPlayer.play();
}
musicPlaying = !musicPlaying;
console.log("DEBUG QML: Setting music enabled to: " + musicPlaying);
py.call("fortunecookie.set_music_enabled", [musicPlaying]);
}
}
}
}
// ================================================================
// MUSIK-BUTTON
// SEITE 1: EINSTELLUNGEN
// ================================================================
// FIX: Label DIREKT verwenden (nicht in Item nesten!) - sonst "Element is not creatable"
// Icon-Größe: xx-large für bessere Sichtbarkeit
// Hintergrund: transparent (wie Page-Hintergrund)
// Erst nach Initialisierung anzeigen, um Flackern zu vermeiden
Label {
id: musicButton
anchors {
right: parent.right
bottom: parent.bottom
margins: units.gu(2)
}
width: units.gu(10)
height: units.gu(10)
text: musicPlaying ? "\uD83D\uDD0A" : "\uD83D\uDD07" // 🎵 oder 🔇
fontSize: "xx-large" // Großes Icon für gute Sichtbarkeit
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: theme.palette.normalText
// FIX: Erst nach Initialisierung anzeigen
visible: appInitialized
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("DEBUG QML: Music button clicked, current musicPlaying: " + musicPlaying);
if (musicPlaying) {
mediaPlayer.stop();
} else {
mediaPlayer.play();
}
musicPlaying = !musicPlaying;
console.log("DEBUG QML: Setting music enabled to: " + musicPlaying);
py.call("fortunecookie.set_music_enabled", [musicPlaying]);
}
}
Settings {
id: settingsPage
// Zugriff auf Medien-Player ueber parent
property MediaPlayer settingsMediaPlayer: mainStack.stackMediaPlayer
property MediaPlayer settingsCrackPlayer: mainStack.stackCrackPlayer
}
}
}
+210
View File
@@ -0,0 +1,210 @@
import QtQuick 2.7
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.3
import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import io.thp.pyotherside 1.4
Page {
id: settingsPage
objectName: 'settingsPage'
property bool settingsInitialized: false
property var currentFortuneList: "classic"
property real musicVolume: 0.5
property real crackVolume: 1.0
header: PageHeader {
title: "Einstellungen"
}
// Timer für PyOtherSide Initialisierung
Timer {
id: initTimer
interval: 1000
running: true
repeat: false
onTriggered: {
try {
// Einstellungen laden
currentFortuneList = py.call_sync("fortunecookie.get_current_fortune_list", []);
musicVolume = py.call_sync("fortunecookie.get_music_volume", []);
crackVolume = py.call_sync("fortunecookie.get_crack_volume", []);
// Slider Werte setzen
musicVolumeSlider.value = musicVolume;
crackVolumeSlider.value = crackVolume;
// Spruchlisten ComboBox füllen
var lists = py.call_sync("fortunecookie.get_fortune_lists", []);
for (var i = 0; i < lists.length; i++) {
fortuneListCombo.model.append(lists[i]);
}
// Aktuelle Liste auswählen
for (var i = 0; i < fortuneListCombo.count; i++) {
if (fortuneListCombo.itemAt(i) === currentFortuneList) {
fortuneListCombo.currentIndex = i;
break;
}
}
settingsInitialized = true;
} catch (e) {
console.log("ERROR: Einstellungen nicht geladen: " + e);
}
}
}
Python {
id: py
Component.onCompleted: {
addImportPath(Qt.resolvedUrl("../src"));
importModule("fortunecookie", function() {
console.log("Python-Modul in Settings geladen");
});
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: units.gu(2)
spacing: units.gu(2)
// ============================================================
// SPRUCHLISTEN-AUSWAHL
// ============================================================
Label {
text: "Spruchliste:"
Layout.fillWidth: true
fontSize: "large"
}
ComboBox {
id: fortuneListCombo
Layout.fillWidth: true
Layout.preferredHeight: units.gu(8)
fontSize: "medium"
onActivated: {
var newList = fortuneListCombo.currentText;
py.call("fortunecookie.set_fortune_list", [newList], function() {
console.log("Spruchliste gewaehlt: " + newList);
});
}
}
// ============================================================
// LAUTSTÄRKE - MUSIK
// ============================================================
Label {
text: "Musik-Lautstärke:"
Layout.fillWidth: true
fontSize: "large"
}
RowLayout {
Layout.fillWidth: true
spacing: units.gu(2)
Slider {
id: musicVolumeSlider
Layout.fillWidth: true
minimumValue: 0.0
maximumValue: 1.0
stepSize: 0.1
value: 0.5
onMoved: {
var volume = musicVolumeSlider.value;
py.call("fortunecookie.set_music_volume", [volume]);
// Aktualisiere MediaPlayer ueber parent
if (parent && parent.parent && parent.parent.parent) {
var mainView = parent.parent.parent;
if (mainView && mainView.globalMediaPlayer) {
mainView.globalMediaPlayer.volume = volume;
}
}
musicVolumeLabel.text = Math.round(volume * 100) + "%";
}
}
Label {
id: musicVolumeLabel
text: Math.round(musicVolumeSlider.value * 100) + "%"
width: units.gu(10)
horizontalAlignment: Text.AlignHCenter
fontSize: "medium"
}
}
// ============================================================
// LAUTSTÄRKE - KNACK-GERÄUSCH
// ============================================================
Label {
text: "Knack-Lautstärke:"
Layout.fillWidth: true
fontSize: "large"
}
RowLayout {
Layout.fillWidth: true
spacing: units.gu(2)
Slider {
id: crackVolumeSlider
Layout.fillWidth: true
minimumValue: 0.0
maximumValue: 1.0
stepSize: 0.1
value: 1.0
onMoved: {
var volume = crackVolumeSlider.value;
py.call("fortunecookie.set_crack_volume", [volume]);
// Aktualisiere MediaPlayer ueber parent
if (parent && parent.parent && parent.parent.parent) {
var mainView = parent.parent.parent;
if (mainView && mainView.globalCrackPlayer) {
mainView.globalCrackPlayer.volume = volume;
}
}
crackVolumeLabel.text = Math.round(volume * 100) + "%";
}
}
Label {
id: crackVolumeLabel
text: Math.round(crackVolumeSlider.value * 100) + "%"
width: units.gu(10)
horizontalAlignment: Text.AlignHCenter
fontSize: "medium"
}
}
// ============================================================
// ZURÜCK-BUTTON
// ============================================================
Item {
Layout.fillWidth: true
Layout.preferredHeight: units.gu(10)
}
Button {
text: "Zurück"
Layout.fillWidth: false
Layout.preferredWidth: units.gu(20)
Layout.preferredHeight: units.gu(8)
Layout.alignment: Qt.AlignHCenter
onClicked: {
mainStack.currentIndex = 0;
}
}
}
}