Skip to content

Commit 3aea355

Browse files
add random cocktail option
1 parent 2207294 commit 3aea355

File tree

12 files changed

+356
-41
lines changed

12 files changed

+356
-41
lines changed

src/config/config_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ class ConfigManager:
117117
MAKER_USE_RECIPE_VOLUME: bool = False
118118
# Option to add the single ingredient option to the maker pane
119119
MAKER_ADD_SINGLE_INGREDIENT: bool = False
120+
# Option to show a random cocktail tile in the maker tab
121+
MAKER_RANDOM_COCKTAIL: bool = False
120122
# List of normal (non-addressable) LED configurations
121123
LED_NORMAL: ClassVar[list[NormalLedConfig]] = []
122124
# List of WS281x (addressable) LED configurations
@@ -225,6 +227,7 @@ def __init__(self) -> None:
225227
"MAKER_CHECK_INTERNET": BoolType(check_name="Check Internet"),
226228
"MAKER_USE_RECIPE_VOLUME": BoolType(check_name="Use Recipe Volume"),
227229
"MAKER_ADD_SINGLE_INGREDIENT": BoolType(check_name="Can Spend Single Ingredient"),
230+
"MAKER_RANDOM_COCKTAIL": BoolType(check_name="Random Cocktail Option"),
228231
"LED_NORMAL": ListType(
229232
DictType(
230233
{

src/language.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,12 @@ ui:
449449
nfc_scan_to_proceed:
450450
en: 'Scan NFC to proceed'
451451
de: 'NFC scannen um fortzufahren'
452+
random_cocktail_label:
453+
en: 'Random Cocktail'
454+
de: 'Zufalls-Cocktail'
455+
random_be_surprised:
456+
en: 'Be Surprised!'
457+
de: 'Lass dich überraschen!'
452458

453459
cocktail_selection:
454460
# best is to use some space before to get a padding for icon
@@ -762,6 +768,9 @@ ui:
762768
MAKER_ADD_SINGLE_INGREDIENT:
763769
en: 'Adds the option to also spend a single ingredient to the cocktails in the maker tab'
764770
de: 'Fügt eine Option zu den Cocktails in dem Maker Tab hinzu, auch einzelne Zutaten ausgeben zu können'
771+
MAKER_RANDOM_COCKTAIL:
772+
en: 'Adds a random cocktail tile to the maker tab that picks a surprise cocktail on prepare'
773+
de: 'Fügt eine Zufalls-Cocktail Kachel zum Maker Tab hinzu, die beim Zubereiten einen Überraschungscocktail wählt'
765774
LED_NORMAL:
766775
en: 'List with config for each normal (non-addressable) LED: pin, default on state, preparation state (on/off/effects)'
767776
de: 'Liste mit Einstellung für jeden normalen (nicht ansteuerbaren) LED: Pin, Standard an-Zustand, Zustand bei Zubereitung (an/aus/Effekte)'

src/ui/cocktail_view.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,37 @@ def _square_size() -> int:
4545
return int(cfg.UI_WIDTH / (n_columns * 1.17))
4646

4747

48+
def generate_random_image_block(cocktails: list[Cocktail], mainscreen: MainScreen) -> QVBoxLayout:
49+
"""Generate an image block for the random cocktail tile."""
50+
square_size = _square_size()
51+
header_font_size = round(square_size / 15.8)
52+
header_height = round(square_size / 6.3)
53+
random_label = UI_LANGUAGE.get_translation("random_cocktail_label", "main_window")
54+
button = create_button(
55+
random_label,
56+
font_size=header_font_size,
57+
min_h=0,
58+
max_h=header_height,
59+
max_w=square_size,
60+
css_class="btn-inverted btn-half-top",
61+
)
62+
label = ClickableLabel(random_label)
63+
label.setProperty("cssClass", "cocktail-picture-view")
64+
pixmap = QPixmap(str(DEFAULT_COCKTAIL_IMAGE))
65+
label.setPixmap(pixmap)
66+
label.setScaledContents(True)
67+
label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
68+
label.setMinimumSize(square_size, square_size)
69+
label.setMaximumSize(square_size, square_size)
70+
layout = QVBoxLayout()
71+
layout.setSpacing(0)
72+
layout.addWidget(button)
73+
layout.addWidget(label)
74+
button.clicked.connect(lambda _, c=cocktails: mainscreen.open_random_cocktail_detail(c))
75+
label.clicked.connect(lambda c=cocktails: mainscreen.open_random_cocktail_detail(c))
76+
return layout
77+
78+
4879
def generate_image_block(cocktail: Cocktail | None, mainscreen: MainScreen) -> QVBoxLayout:
4980
"""Generate a image block for the given cocktail."""
5081
# those factors are taken from calculations based on the old static values
@@ -193,16 +224,22 @@ def _populate_cocktails_grid(self) -> None:
193224
cocktails = [c for c in cocktails if c.is_allowed]
194225
# sort cocktails by name
195226
cocktails.sort(key=lambda x: x.name.lower())
227+
# optionally prepend the random cocktail tile
228+
offset = 0
229+
if cfg.MAKER_RANDOM_COCKTAIL and cocktails:
230+
block = generate_random_image_block(cocktails, self.mainscreen)
231+
self.grid.addLayout(block, 0, 0)
232+
offset = 1
196233
# fill the grid with n_columns columns, then go to another row
197-
for i in range(0, len(cocktails), n_columns):
198-
for j in range(n_columns):
199-
if i + j >= len(cocktails):
200-
break
201-
block = generate_image_block(cocktails[i + j], self.mainscreen)
202-
self.grid.addLayout(block, i // n_columns, j)
234+
for idx, cocktail in enumerate(cocktails):
235+
pos = idx + offset
236+
row = pos // n_columns
237+
col = pos % n_columns
238+
block = generate_image_block(cocktail, self.mainscreen)
239+
self.grid.addLayout(block, row, col)
203240
# Optionally add the single ingredient block after all cocktails
204241
if cfg.MAKER_ADD_SINGLE_INGREDIENT and not cfg.payment_enabled:
205-
total = len(cocktails)
242+
total = len(cocktails) + offset
206243
row = total // n_columns
207244
col = total % n_columns
208245
block = generate_image_block(None, self.mainscreen)

src/ui/setup_cocktail_selection.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import random
34
from typing import TYPE_CHECKING
45

56
from PyQt6.QtGui import QFont, QPixmap
@@ -9,6 +10,7 @@
910
from src.database_commander import DB_COMMANDER
1011
from src.dialog_handler import UI_LANGUAGE
1112
from src.display_controller import DP_CONTROLLER
13+
from src.filepath import DEFAULT_COCKTAIL_IMAGE
1214
from src.image_utils import find_cocktail_image
1315
from src.models import Cocktail, Ingredient
1416
from src.ui.creation_utils import LARGE_FONT, create_button, create_label, set_underline
@@ -23,12 +25,20 @@
2325
class CocktailSelection(QDialog, Ui_CocktailSelection):
2426
"""Class for the Cocktail selection view."""
2527

26-
def __init__(self, mainscreen: MainScreen, cocktail: Cocktail) -> None:
28+
def __init__(
29+
self,
30+
mainscreen: MainScreen,
31+
cocktail: Cocktail,
32+
random_mode: bool = False,
33+
random_pool: list[Cocktail] | None = None,
34+
) -> None:
2735
super().__init__(parent=mainscreen)
2836
self.setupUi(self)
2937
DP_CONTROLLER.initialize_window_object(self)
3038
self.cocktail = cocktail
3139
self.mainscreen = mainscreen
40+
self.random_mode = random_mode
41+
self.random_pool = random_pool or []
3242
# Store references for dynamic button label updates
3343
self._volume_buttons: list[tuple[int, QPushButton]] = [] # list of (volume, button) tuples
3444
# build the image
@@ -94,8 +104,53 @@ def set_cocktail(self, cocktail: Cocktail) -> None:
94104
self.cocktail = cocktail
95105
self._set_image()
96106

107+
def update_random_display(self) -> None:
108+
"""Update the display for random cocktail mode."""
109+
pixmap = QPixmap(str(DEFAULT_COCKTAIL_IMAGE))
110+
self.image_container.setPixmap(pixmap)
111+
random_label = UI_LANGUAGE.get_translation("random_cocktail_label", "main_window")
112+
surprise_label = UI_LANGUAGE.get_translation("random_be_surprised", "main_window")
113+
self.LAlkoholname.setText(random_label)
114+
self.LAlkoholgehalt.setText("?")
115+
self.LMenge.setText("")
116+
# hide alcohol low/high buttons
117+
self.increase_alcohol.setVisible(False)
118+
self.decrease_alcohol.setVisible(False)
119+
# show virgin toggle only if any cocktail in pool has virgin_available
120+
has_virgin = any(c.virgin_available for c in self.random_pool)
121+
self.virgin_toggle.setVisible(has_virgin)
122+
self.virgin_toggle.setChecked(False)
123+
# show surprise message in first ingredient label, clear rest
124+
fields_ingredient = self.get_labels_maker_ingredients()
125+
fields_volume = self.get_labels_maker_volume()
126+
for field_ingredient, field_volume in zip(fields_ingredient, fields_volume):
127+
field_ingredient.setText("")
128+
field_volume.setText("")
129+
if fields_ingredient:
130+
fields_ingredient[0].setText(surprise_label)
131+
132+
def _prepare_random_cocktail(self, amount: int) -> None:
133+
"""Pick a random cocktail from the pool and prepare it."""
134+
if self.is_virgin:
135+
pool = [c for c in self.random_pool if c.virgin_available]
136+
else:
137+
pool = [c for c in self.random_pool if not c.only_virgin]
138+
if not pool:
139+
return
140+
chosen = random.choice(pool)
141+
# Re-fetch from DB for up-to-date data
142+
db_cocktail = DB_COMMANDER.get_cocktail(chosen.id)
143+
if db_cocktail is not None:
144+
chosen = db_cocktail
145+
self.cocktail = chosen
146+
self._scale_cocktail(amount)
147+
qt_prepare_flow(self.mainscreen, self.cocktail)
148+
97149
def _prepare_cocktail(self, amount: int) -> None:
98150
"""Prepare the cocktail and switches to the maker screen, if successful."""
151+
if self.random_mode:
152+
self._prepare_random_cocktail(amount)
153+
return
99154
# same applies here, need to refetch the cocktail from db
100155
db_cocktail = DB_COMMANDER.get_cocktail(self.cocktail.id)
101156
if db_cocktail is not None:
@@ -245,7 +300,8 @@ def _toggle_virgin(self, _: bool) -> None:
245300
"""Toggle the virgin option."""
246301
self.decrease_alcohol.setChecked(False)
247302
self.increase_alcohol.setChecked(False)
248-
self.update_cocktail_data()
303+
if not self.random_mode:
304+
self.update_cocktail_data()
249305

250306
def _adjust_preparation_buttons(self) -> None:
251307
"""Decide if to use a single or multiple buttons and adjusts the text accordingly.

src/ui/setup_mainwindow.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,22 +228,34 @@ def _handle_restricted_mode_click(self, index: int) -> None:
228228
# Remove the event filter since it's no longer needed
229229
self.tabWidget.tabBar().removeEventFilter(self) # type: ignore
230230

231-
def open_cocktail_detail(self, cocktail: Cocktail) -> None:
232-
"""Open the cocktail selection screen."""
231+
def _cleanup_cocktail_selection(self) -> None:
232+
"""Clean up any existing cocktail selection widget."""
233233
if self.cocktail_selection is not None:
234-
# Clean up all internal layouts and widgets recursively
235234
DP_CONTROLLER.delete_items_of_layout(self.cocktail_selection.layout())
236-
# Remove from stacked widget
237235
self.container_maker.removeWidget(self.cocktail_selection)
238-
# Schedule for deletion and remove reference
239236
self.cocktail_selection.deleteLater()
240237
self.cocktail_selection = None
238+
239+
def open_cocktail_detail(self, cocktail: Cocktail) -> None:
240+
"""Open the cocktail selection screen."""
241+
self._cleanup_cocktail_selection()
241242
self.cocktail_selection = CocktailSelection(self, cocktail)
242243
self.container_maker.addWidget(self.cocktail_selection)
243244
self.cocktail_selection.set_cocktail(cocktail)
244245
self.cocktail_selection.update_cocktail_data()
245246
self.switch_to_cocktail_detail()
246247

248+
def open_random_cocktail_detail(self, cocktails: list[Cocktail]) -> None:
249+
"""Open the cocktail selection screen in random mode."""
250+
if not cocktails:
251+
return
252+
self._cleanup_cocktail_selection()
253+
# Use the first cocktail as a placeholder; actual cocktail is chosen at prepare time
254+
self.cocktail_selection = CocktailSelection(self, cocktails[0], random_mode=True, random_pool=cocktails)
255+
self.container_maker.addWidget(self.cocktail_selection)
256+
self.cocktail_selection.update_random_display()
257+
self.switch_to_cocktail_detail()
258+
247259
def switch_to_cocktail_detail(self) -> None:
248260
if self.cocktail_selection is None:
249261
return

web_client/src/components/cocktail/CocktailList.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type React from 'react';
22
import { useState } from 'react';
33
import { useTranslation } from 'react-i18next';
4+
import { FaQuestion } from 'react-icons/fa';
45
import { MdNoDrinks } from 'react-icons/md';
56
import Modal from 'react-modal';
67
import { useCocktails } from '../../api/cocktails';
@@ -15,6 +16,7 @@ import LockScreen from '../common/LockScreen';
1516
import SearchBar from '../common/SearchBar';
1617
import UserDisplay from '../common/UserDisplay';
1718
import CocktailSelection from './CocktailSelection';
19+
import RandomCocktailSelection from './RandomCocktailSelection';
1820
import SingleIngredientSelection from './SingleIngredientSelection';
1921

2022
const CocktailList: React.FC = () => {
@@ -23,6 +25,7 @@ const CocktailList: React.FC = () => {
2325
const { data: cocktails, error, isLoading } = useCocktails(true, config.MAKER_MAX_HAND_INGREDIENTS ?? 0);
2426
const [selectedCocktail, setSelectedCocktail] = useState<Cocktail | null>(null);
2527
const [singleIngredientOpen, setSingleIngredientOpen] = useState(false);
28+
const [randomCocktailOpen, setRandomCocktailOpen] = useState(false);
2629
const [search, setSearch] = useState<string | null>(null);
2730
const [showOnlyVirginPossible, setShowOnlyVirginPossible] = useState(false);
2831
const { t } = useTranslation();
@@ -99,6 +102,25 @@ const CocktailList: React.FC = () => {
99102
</div>
100103
</div>
101104
<div className='flex flex-wrap gap-3 justify-center items-center w-full mb-4'>
105+
{config.MAKER_RANDOM_COCKTAIL && displayedCocktails && displayedCocktails.length > 0 && !search && (
106+
<button
107+
className='border-2 border-primary active:border-secondary rounded-xl box-border overflow-hidden min-w-56 max-w-64 basis-1 grow text-xl font-bold bg-primary active:bg-secondary text-background'
108+
onClick={() => setRandomCocktailOpen(true)}
109+
type='button'
110+
>
111+
<p className='text-center py-1 flex items-center justify-center'>
112+
<FaQuestion className='mr-2' />
113+
{t('cocktails.randomCocktail')}
114+
</p>
115+
<div className='relative w-full' style={{ paddingTop: '100%' }}>
116+
<img
117+
src={`${API_URL}/static/default/default.jpg`}
118+
alt={t('cocktails.randomCocktail')}
119+
className='absolute top-0 left-0 w-full h-full object-cover'
120+
/>
121+
</div>
122+
</button>
123+
)}
102124
{displayedCocktails
103125
?.sort((a, b) => a.name.localeCompare(b.name))
104126
.map((cocktail) => {
@@ -176,6 +198,19 @@ const CocktailList: React.FC = () => {
176198
<Modal isOpen={singleIngredientOpen} className='modal slim' overlayClassName='overlay z-20' preventScroll={true}>
177199
<SingleIngredientSelection onClose={() => setSingleIngredientOpen(false)} />
178200
</Modal>
201+
<Modal
202+
isOpen={randomCocktailOpen}
203+
onRequestClose={() => setRandomCocktailOpen(false)}
204+
contentLabel='Random Cocktail'
205+
className='modal'
206+
overlayClassName='overlay z-20'
207+
preventScroll={true}
208+
>
209+
<RandomCocktailSelection
210+
handleCloseModal={() => setRandomCocktailOpen(false)}
211+
cocktails={displayedCocktails || []}
212+
/>
213+
</Modal>
179214
</div>
180215
);
181216
};

web_client/src/components/cocktail/CocktailSelection.tsx

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import type React from 'react';
22
import { useEffect, useState } from 'react';
3-
import { FaGlassMartiniAlt, FaSkullCrossbones, FaWineGlassAlt } from 'react-icons/fa';
3+
import { FaSkullCrossbones } from 'react-icons/fa';
44
import { GrFormNextLink, GrFormPreviousLink } from 'react-icons/gr';
5-
import { ImMug } from 'react-icons/im';
65
import { IoIosHappy } from 'react-icons/io';
76
import { MdNoDrinks } from 'react-icons/md';
8-
import { PiPintGlassFill } from 'react-icons/pi';
9-
import { TbGlassChampagne } from 'react-icons/tb';
107
import { prepareCocktail } from '../../api/cocktails';
118
import { API_URL } from '../../api/common';
129
import { Tabs } from '../../constants/tabs';
@@ -18,6 +15,7 @@ import CloseButton from '../common/CloseButton';
1815
import ProgressModal from './ProgressModal';
1916
import RefillPrompt from './RefillPrompt';
2017
import TeamSelection from './TeamSelection';
18+
import { FALLBACK_SERVING_SIZES, getServingSizeIconIndex, servingSizeIcons } from './utils';
2119

2220
interface CocktailModalProps {
2321
selectedCocktail: Cocktail;
@@ -28,8 +26,6 @@ interface CocktailModalProps {
2826

2927
type alcoholState = 'high' | 'low' | 'normal' | 'virgin';
3028

31-
const fallbackServingSize = [200, 250, 300];
32-
const icons = [TbGlassChampagne, FaWineGlassAlt, FaGlassMartiniAlt, PiPintGlassFill, ImMug];
3329
const alcoholFactor = {
3430
high: 1.25,
3531
low: 0.75,
@@ -57,7 +53,7 @@ const CocktailSelection: React.FC<CocktailModalProps> = ({
5753
const { config } = useConfig();
5854
const possibleServingSizes = config.MAKER_USE_RECIPE_VOLUME
5955
? [displayCocktail.amount]
60-
: (config.MAKER_PREPARE_VOLUME ?? fallbackServingSize);
56+
: (config.MAKER_PREPARE_VOLUME ?? FALLBACK_SERVING_SIZES);
6157

6258
useEffect(() => {
6359
const initialAlcoholState = selectedCocktail.only_virgin ? 'virgin' : 'normal';
@@ -120,23 +116,6 @@ const CocktailSelection: React.FC<CocktailModalProps> = ({
120116
.filter((ingredient) => ingredient.hand)
121117
.sort((a, b) => b.amount - a.amount);
122118

123-
const getIconIndex = (idx: number) => {
124-
const totalIcons = icons.length;
125-
const needed = Math.min(possibleServingSizes.length, totalIcons);
126-
const center = Math.floor(totalIcons / 2);
127-
128-
// Choose a centered contiguous window; for even sizes bias to the right.
129-
let start = needed % 2 === 1 ? center - Math.floor(needed / 2) : center - needed / 2 + 1;
130-
131-
if (start < 0) start = 0;
132-
if (start + needed > totalIcons) start = totalIcons - needed;
133-
134-
// If more buttons than icons, clamp to last icon.
135-
if (idx >= needed) return totalIcons - 1;
136-
137-
return start + idx;
138-
};
139-
140119
const calculateDisplayPrice = (amount: number, pricePer100: number): string => {
141120
if (config.PAYMENT_TYPE === 'Disabled') return '';
142121
const virginMultiplier = alcohol === 'virgin' ? config.PAYMENT_VIRGIN_MULTIPLIER / 100 : 1;
@@ -256,7 +235,7 @@ const CocktailSelection: React.FC<CocktailModalProps> = ({
256235
onClick={() => prepareCocktailClick(amount)}
257236
textSize='lg'
258237
className='w-full'
259-
icon={icons[getIconIndex(index)]}
238+
icon={servingSizeIcons[getServingSizeIconIndex(index, possibleServingSizes.length)]}
260239
iconSize={25}
261240
/>
262241
))}

0 commit comments

Comments
 (0)