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