fix: Last-State, Keks-Größe, Musik-Button

- Last-State speichert in .cache/fortunecookie.darklithium/music_enabled
- Geöffneter Keks: 36x28 gu (geschlossen: 32x24 gu)
- Musik-Button: 10x10 gu, x-large Icon, erst nach Init sichtbar
- Python ohne PySide2-Abhängigkeiten
- Properties vor Functions in QML

Fixes: PermissionError auf .config/, Icon-Flickern

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
darklithium
2026-06-02 03:27:35 +02:00
parent 1a1092bb2b
commit ace4d9c43c
8 changed files with 151 additions and 209 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ Type=Application
Name=Fortune Cookie
Comment=Glückskeks App mit Sprüchen, Musik und einfacher Verwaltung
Exec=qmlscene %U qml/Main.qml
Icon=assets/cookie_closed.png
Icon=assets/cookie_closed2.png
Terminal=false
Categories=Utility;
X-Lomiri-Touch=true
+75 -47
View File
@@ -1,5 +1,6 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtMultimedia 5.0
import Lomiri.Components 1.3
import Lomiri.Components.Popups 1.3
import io.thp.pyotherside 1.4
@@ -11,97 +12,114 @@ MainView {
height: units.gu(75)
theme.name: "Lomiri.Components.Themes.SuruDark"
// ====================================================================
// 1. PYTHON-MODUL (STANDARD 1.7)
// ====================================================================
property bool fortuneOpened: false
property string currentFortune: ""
property bool musicPlaying: false
property bool musicButtonVisible: false
Python {
id: py
Component.onCompleted: {
addImportPath(Qt.resolvedUrl("../src"));
importModule("fortunecookie", function() {
console.log("Python-Modul fortunecookie geladen");
// Initialisierung
currentFortuneLabel.text = py.call_sync("fortunecookie.get_initial_fortune", []);
cookieImage.source = "assets/cookie_closed.png";
fortuneOpened = false;
});
}
}
// ====================================================================
// 2. APP-ZUSTAND (STANDARD 1.7)
// ====================================================================
property bool fortuneOpened: false
property string currentFortune: ""
property bool musicPlaying: false
MediaPlayer {
id: mediaPlayer
source: Qt.resolvedUrl("../assets/chinese_music.mp3")
loops: MediaPlayer.Infinite
volume: 0.5
}
MediaPlayer {
id: crackMediaPlayer
source: Qt.resolvedUrl("../assets/cookie_crack.mp3")
volume: 1.0
}
// ====================================================================
// 3. HAUPTSEITE
// ====================================================================
Page {
id: mainPage
anchors.fill: parent
// Header
header: PageHeader {
title: "Fortune Cookie"
}
// ================================================================
// COOKIE & SPRUCH
// ================================================================
Timer {
id: initTimer
interval: 1000 // Warte 1 Sekunde auf Python-Ladung
running: true
repeat: false
onTriggered: {
try {
currentFortuneLabel.text = py.call_sync("fortunecookie.get_initial_fortune", []);
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
musicPlaying = py.call_sync("fortunecookie.get_music_enabled", []);
console.log("DEBUG QML: musicPlaying loaded from Python: " + musicPlaying);
if (musicPlaying) {
mediaPlayer.play();
}
musicButtonVisible = true;
} catch (e) {
console.log("ERROR QML: Failed to initialize: " + e);
}
}
}
// Cookie-Image (zentral)
Image {
id: cookieImage
anchors.centerIn: parent
width: units.gu(30)
height: units.gu(30)
source: fortuneOpened ? "assets/cookie_open.png" : "assets/cookie_closed.png"
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
// Wisch-Geste nach oben
MouseArea {
anchors.fill: parent
property real startY: 0
onPressed: {
startY = mouseY
}
onPressed: startY = mouseY
onReleased: {
// Ende der Geste - pruufen ob nach oben gewischt
if (mouseY < startY - units.gu(2)) {
// Wisch nach oben -> Cookie oeffnen
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = "assets/cookie_open.png";
cookieImage.source = Qt.resolvedUrl("../assets/cookie_open2.png");
});
}
}
}
// Tap auf Cookie (wenn geschlossen)
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!fortuneOpened) {
// Cookie oeffnen (gleiche Funktion wie Wisch nach oben)
py.call("fortunecookie.open_fortune", [], function() {
crackMediaPlayer.play();
fortuneOpened = true;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = "assets/cookie_open.png";
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");
}
}
}
}
// Fortune-Text (erscheint nach dem Oeffnen)
Label {
id: currentFortuneLabel
anchors {
@@ -118,7 +136,6 @@ MainView {
visible: fortuneOpened
wrapMode: Text.WordWrap
// Tap auf Spruch -> neuer Cookie
MouseArea {
anchors.fill: parent
@@ -127,34 +144,45 @@ MainView {
fortuneOpened = false;
currentFortune = py.call_sync("fortunecookie.get_current_fortune", []);
currentFortuneLabel.text = currentFortune;
cookieImage.source = "assets/cookie_closed.png";
cookieImage.source = Qt.resolvedUrl("../assets/cookie_closed2.png");
});
}
}
}
// ================================================================
// MUSIK BUTTON (rechts unten)
// ================================================================
Button {
id: musicButton
Item {
id: musicButtonContainer
anchors {
right: parent.right
bottom: parent.bottom
margins: units.gu(2)
}
width: units.gu(8)
height: units.gu(8)
text: musicPlaying ? "\uD83D\uDD07" : "\uD83D\uDD0A"
width: units.gu(10)
height: units.gu(10)
visible: musicButtonVisible
Label {
id: musicButton
text: musicPlaying ? "\uD83D\uDD0A" : "\uD83D\uDD07"
fontSize: "x-large"
anchors.centerIn: parent
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("DEBUG QML: Music button clicked, current musicPlaying: " + musicPlaying);
if (musicPlaying) {
py.call("fortunecookie.stop_music", []);
mediaPlayer.stop();
} else {
py.call("fortunecookie.start_music", []);
mediaPlayer.play();
}
musicPlaying = !musicPlaying;
console.log("DEBUG QML: Setting music enabled to: " + musicPlaying);
py.call("fortunecookie.set_music_enabled", [musicPlaying]);
}
}
}
}
}
+69 -155
View File
@@ -1,6 +1,7 @@
"""
Fortune Cookie v1.0 - Python Backend Module
Framework 1.7 Standard
Audio-Steuerung in QML (keine Qt-Python-Bindings benoetigt)
"""
import os
@@ -37,18 +38,13 @@ _current_fortune = ""
_fortunes = []
_initialized = False
# Musik-Status
_music_enabled = True
_music_playing = False
# Medien-Player (wird lazy initialisiert)
_media_player = None
_cookie_crack_sound = None
# Musik-Status (wird dynamisch von Datei geladen)
# _music_enabled wird nicht als globale Variable gespeichert, sondern immer frisch geladen
def _init():
"""Initialisiert das Modul (wird beim ersten Aufruf ausgefuehrt)."""
global _fortunes, _initialized, _music_enabled
global _fortunes, _initialized
if _initialized:
return True
@@ -56,9 +52,6 @@ def _init():
# Lade Fortunes
_load_fortunes()
# Lade Last-State
_music_enabled = _load_music_state()
_initialized = True
return True
@@ -84,6 +77,9 @@ def _load_fortunes():
if isinstance(data, list):
_fortunes = data
elif isinstance(data, dict):
# Lade deutsche Sprüche, falls vorhanden, sonst englische
_fortunes = data.get("de", data.get("en", []))
elif isinstance(data, dict) and "fortunes" in data:
_fortunes = data.get("fortunes", [])
return
@@ -95,12 +91,14 @@ def _load_fortunes():
]
except Exception:
# Fallback: Einige Standard-Sprueche
_fortunes = [
"Ein guter Tag beginnt mit einem Laecheln.",
"Das Glueck liegt in den kleinen Dingen.",
"Geduld ist eine Tugend.",
]
_fortunes = []
def _get_random_fortune():
"""Gibt einen zufaelligen Spruch zurueck."""
if not _fortunes:
_load_fortunes()
return random.choice(_fortunes) if _fortunes else "Keine Sprueche verfguebar."
def get_initial_fortune():
@@ -112,16 +110,13 @@ def get_initial_fortune():
def open_fortune():
"""Oeffnet den Fortune Cookie (neuer Spruch + Knack-Geraeusch)."""
"""Oeffnet den Fortune Cookie (neuer Spruch)."""
_init()
global _current_fortune
# Neuer Spruch
_current_fortune = _get_random_fortune()
# Knack-Geraeusch abspielen (wenn verfguebar)
_play_crack_sound()
return True
@@ -140,164 +135,83 @@ def get_new_fortune():
return _current_fortune
def _get_random_fortune():
"""Gibt einen zufaelligen Spruch zurueck."""
if not _fortunes:
_load_fortunes()
return random.choice(_fortunes) if _fortunes else "Keine Sprueche verfguebar."
# ============================================================================
# MUSIK-LOGIK
# LAST-STATE SPEICHERUNG (Musik an/aus)
# ============================================================================
def start_music():
"""Startet die Hintergrundmusik."""
_init()
global _music_playing, _media_player, _music_enabled
if not _music_enabled:
return False
def _get_config_dir():
"""Gibt das Konfigurationsverzeichnis der App zurueck.
Click-Apps auf UBPorts haben eingeschraenkte Schreibrechte.
Verwendete Pfade:
- ~/.cache/<appname>/ (funktioniert in Click-Apps)
"""
try:
# Medien-Player initialisieren (wenn nicht vorhanden)
if _media_player is None:
from PySide2 import QtMultimedia, QtCore
_media_player = QtMultimedia.QMediaPlayer()
audio_output = QtMultimedia.QAudioOutput()
_media_player.setAudioOutput(audio_output)
# Musik-Datei laden
music_path = get_asset_path("chinese_music.mp3")
_media_player.setSource(QtCore.QUrl.fromLocalFile(music_path))
_media_player.setLoops(QtMultimedia.QMediaPlayer.Infinite)
_media_player.setVolume(50)
_media_player.play()
_music_playing = True
return True
home = os.path.expanduser("~")
# Click-App-Pfad (funktioniert in der Sandbox)
app_name = "fortunecookie.darklithium"
cache_dir = os.path.join(home, ".cache", app_name)
os.makedirs(cache_dir, exist_ok=True)
return cache_dir
except Exception:
return False
# Fallback
return os.path.join("/tmp", "fortunecookie")
def stop_music():
"""Stoppt die Hintergrundmusik."""
global _music_playing, _media_player
if _media_player is not None:
try:
_media_player.stop()
except Exception:
pass
_music_playing = False
return True
def toggle_music():
"""Wechselt Musik-Status (an/aus)."""
if _music_playing:
return stop_music()
else:
return start_music()
def set_music_enabled(enabled):
"""Aktiviert/Deaktiviert Musik generell."""
global _music_enabled
_music_enabled = enabled
_save_music_state(enabled)
return enabled
def get_music_enabled():
"""Gibt zurueck, ob Musik aktiviert ist."""
global _music_enabled
return _music_enabled
def get_music_playing():
"""Gibt zurueck, ob Musik gerade spielt."""
global _music_playing
return _music_playing
# ============================================================================
# LAST-STATE SPEICHERUNG
# ============================================================================
def _get_data_dir():
"""Gibt das Datenverzeichnis zurueck."""
if "CLICK" in os.environ:
return os.path.join(
os.path.expanduser("~"), ".local", "share", f"{APP_NAME}.darklithium"
)
else:
return os.path.join(
os.path.expanduser("~"), ".local", "share", f"{APP_NAME}"
)
def _get_music_state_file():
"""Gibt den Pfad zur Musik-Status-Datei zurueck."""
config_dir = _get_config_dir()
return os.path.join(config_dir, "music_enabled")
def _load_music_state():
"""Laedt den Musik-Status aus Datei."""
"""Laedt den Musik-Status aus Datei (true/false)."""
try:
state_file = os.path.join(_get_data_dir(), "music_state.json")
state_file = _get_music_state_file()
print(f"DEBUG: Loading music state from: {state_file}")
print(f"DEBUG: File exists: {os.path.exists(state_file)}")
if os.path.exists(state_file):
with open(state_file, "r") as f:
data = json.load(f)
return data.get("enabled", True)
except Exception:
pass
content = f.read().strip().lower()
print(f"DEBUG: File content: '{content}'")
result = content == "true"
print(f"DEBUG: Music enabled: {result}")
return result
else:
print("DEBUG: Music state file does not exist, using default: True")
except Exception as e:
print(f"WARN: Musik-Status nicht geladen: {e}")
import traceback
traceback.print_exc()
# Default: Musik an
return True
def _save_music_state(enabled):
"""Speichert den Musik-Status in Datei."""
"""Speichert den Musik-Status in Datei (true/false)."""
try:
data_dir = _get_data_dir()
os.makedirs(data_dir, exist_ok=True)
state_file = os.path.join(data_dir, "music_state.json")
config_dir = _get_config_dir()
os.makedirs(config_dir, exist_ok=True)
state_file = _get_music_state_file()
print(f"DEBUG: Saving music state {enabled} to: {state_file}")
with open(state_file, "w") as f:
json.dump({"enabled": enabled}, f)
except Exception:
pass
f.write("true" if enabled else "false")
print(f"DEBUG: Successfully saved music state")
except Exception as e:
print(f"WARN: Musik-Status nicht gespeichert: {e}")
import traceback
traceback.print_exc()
def _play_crack_sound():
"""Spielt das Knack-Geraeusch ab."""
try:
global _cookie_crack_sound
if _cookie_crack_sound is None:
from PySide2 import QtMultimedia, QtCore
_cookie_crack_sound = QtMultimedia.QMediaPlayer()
audio_output = QtMultimedia.QAudioOutput()
_cookie_crack_sound.setAudioOutput(audio_output)
crack_path = get_asset_path("cookie_crack.mp3")
if os.path.exists(crack_path):
_cookie_crack_sound.setSource(QtCore.QUrl.fromLocalFile(crack_path))
_cookie_crack_sound.setVolume(100)
_cookie_crack_sound.play()
except Exception:
pass
def set_music_enabled(enabled):
"""Aktiviert/Deaktiviert die Musik und speichert den Status."""
_save_music_state(enabled)
return True
# ============================================================================
# DATENVERZEICHNIS (fuer zukuenftige Erweiterungen)
# ============================================================================
def get_data_dir():
"""Gibt das Datenverzeichnis der App zurueck."""
return _get_data_dir()
def get_fortunes_file_path():
"""Gibt den Pfad zur Fortunes-Datei zurueck."""
return os.path.join(_get_data_dir(), "fortunes.json")
def get_music_enabled():
"""Gibt den Musik-Status zurueck (frisch von Datei geladen)."""
return _load_music_state()
# ============================================================================