fix: dynamic height layout + bundle mysql locales to fix 'no localization for eng' error\n\n- Replace all fixed pixel heights in content_layout with size_hint_y proportions\n so the 6 rows fill available space on any screen (800p, 1080p, etc.)\n- Remove AnchorLayout wrapper that caused dead space above content\n- Bundle mysql/connector/locales in PyInstaller build (spec + build_windows.py)\n- Add mysql.connector.plugins hidden imports to spec and build script"

This commit is contained in:
2026-04-07 16:52:10 +03:00
parent b51e8bcc2a
commit 0f7e157406
4 changed files with 78 additions and 83 deletions

View File

@@ -1,21 +1,31 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_all import os
mysql_datas, mysql_binaries, mysql_hiddenimports = collect_all('mysql.connector') # Bundle mysql-connector locales to fix "no localization for language 'eng'" error
_mysql_locales = os.path.join(
os.path.dirname(os.path.abspath('.')),
'db_interface', 'venv', 'Lib', 'site-packages', 'mysql', 'connector', 'locales'
)
# Fallback: resolve relative to spec file location
_spec_dir = os.path.dirname(os.path.abspath(SPEC))
_mysql_locales = os.path.join(_spec_dir, 'venv', 'Lib', 'site-packages', 'mysql', 'connector', 'locales')
a = Analysis( a = Analysis(
['main.py'], ['main.py'],
pathex=[], pathex=[],
binaries=mysql_binaries, binaries=[],
datas=mysql_datas, datas=[
hiddenimports=mysql_hiddenimports + [ (_mysql_locales, os.path.join('mysql', 'connector', 'locales')),
'mysql', 'mysql.connector', 'mysql.connector.locales', ],
'mysql.connector.locales.eng', 'mysql.connector.locales.eng.client_error', hiddenimports=[
'mysql.connector.plugins', 'mysql.connector.plugins.mysql_native_password', 'mysql.connector',
'mysql.connector.locales',
'mysql.connector.locales.eng',
'mysql.connector.plugins',
'mysql.connector.plugins.mysql_native_password',
'mysql.connector.plugins.caching_sha2_password', 'mysql.connector.plugins.caching_sha2_password',
'mysql.connector.aio', 'mysql.connector.aio.plugins', 'kivy.core.window.window_sdl2',
'mysql.connector.aio.plugins.mysql_native_password', 'win32timezone',
'kivy.core.window.window_sdl2', 'win32timezone',
], ],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@@ -22,6 +22,13 @@ def build_executable():
else: else:
print("No icon file found (optional)") print("No icon file found (optional)")
# Locate mysql connector locales inside the venv
import site
venv_site = os.path.join('venv', 'Lib', 'site-packages')
mysql_locales = os.path.join(venv_site, 'mysql', 'connector', 'locales')
add_data_sep = ';' if sys.platform == 'win32' else ':'
mysql_locales_arg = f'--add-data={mysql_locales}{add_data_sep}mysql/connector/locales'
# PyInstaller command - simplified to avoid module collection issues # PyInstaller command - simplified to avoid module collection issues
cmd = [ cmd = [
'pyinstaller', 'pyinstaller',
@@ -29,11 +36,17 @@ def build_executable():
'--onefile', '--onefile',
'--windowed', '--windowed',
'--hidden-import=mysql.connector', '--hidden-import=mysql.connector',
'--hidden-import=mysql.connector.locales',
'--hidden-import=mysql.connector.locales.eng',
'--hidden-import=mysql.connector.plugins',
'--hidden-import=mysql.connector.plugins.mysql_native_password',
'--hidden-import=mysql.connector.plugins.caching_sha2_password',
'--hidden-import=kivy.core.window.window_sdl2', '--hidden-import=kivy.core.window.window_sdl2',
'--hidden-import=win32timezone', '--hidden-import=win32timezone',
'--exclude-module=_tkinter', '--exclude-module=_tkinter',
'--exclude-module=matplotlib', '--exclude-module=matplotlib',
'--exclude-module=numpy', '--exclude-module=numpy',
mysql_locales_arg,
] + icon_param + ['main.py'] ] + icon_param + ['main.py']
try: try:

BIN
dist/DatabaseApp.exe vendored

Binary file not shown.

88
main.py
View File

@@ -3,7 +3,6 @@ from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'system') Config.set('kivy', 'keyboard_mode', 'system')
from kivy.app import App from kivy.app import App
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.floatlayout import FloatLayout from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label from kivy.uix.label import Label
@@ -26,51 +25,26 @@ class DatabaseApp(App):
Window.fullscreen = 'auto' Window.fullscreen = 'auto'
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Responsive sizing: derive all dimensions from the actual screen height # Responsive sizing: derive dimensions from the actual screen height
# so the layout fits on any display (800p, 900p, 1080p, etc.)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
wh = Window.height # actual screen height after fullscreen wh = Window.height
# Numpad occupies 29% of screen height (fixed proportion) # Scale factor (1.0 at 1080p, down to 0.65 on small screens)
h_numpad_wr = int(wh * 0.29)
# Main layout outer padding and spacing (scaled)
s = max(0.65, min(1.0, wh / 1080.0)) s = max(0.65, min(1.0, wh / 1080.0))
pad_v = max(8, int(20 * s)) # top / bottom padding
pad_h = max(8, int(30 * s)) # left / right padding
m_spacing = max(4, int(10 * s)) # gap between content and numpad
# Space available for the 6 content rows (after numpad + padding + gap) # Padding / spacing (all scaled)
avail_total = wh - h_numpad_wr - 2 * pad_v - m_spacing pad_v = max(8, int(20 * s))
c_spacing = max(4, int(12 * s)) # gap between content rows pad_h = max(8, int(30 * s))
avail_items = avail_total - c_spacing * 5 # 6 rows → 5 gaps m_spacing = max(4, int(10 * s))
c_spacing = max(4, int(12 * s))
# Distribute height proportionally among rows sp = max(6, int(10 * s))
# Reference weights: title=50, search=100, mode=38, buttons=65, update=187, status=40
_w = [50, 100, 38, 65, 187, 40]
_t = sum(_w)
def _h(weight):
return max(24, int(avail_items * weight / _t))
h_title = _h(_w[0])
h_search = _h(_w[1])
h_row = max(20, h_search // 2)
h_mode = _h(_w[2])
h_buttons = _h(_w[3])
h_update = _h(_w[4])
h_status = _h(_w[5])
# Update-frame internal heights
upd_pad = max(4, int(8 * s)) upd_pad = max(4, int(8 * s))
upd_spc = max(4, int(8 * s)) upd_spc = max(4, int(8 * s))
h_upd_title = max(20, int(h_update * 0.20))
h_upd_row = max(20, int(h_update * 0.24))
h_upd_inputs = h_upd_row * 2 + upd_spc
h_upd_btns = max(28, h_update - h_upd_title - h_upd_inputs - 2*upd_pad - 2*upd_spc)
# Numpad internal heights
np_pad_v = max(3, int(6 * s)) np_pad_v = max(3, int(6 * s))
np_spc = max(3, int(6 * s)) np_spc = max(3, int(6 * s))
# Numpad (fixed height at bottom 29 % of screen)
h_numpad_wr = int(wh * 0.29)
h_enter_btn = max(34, int(h_numpad_wr * 0.24)) h_enter_btn = max(34, int(h_numpad_wr * 0.24))
h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc h_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
@@ -83,7 +57,11 @@ class DatabaseApp(App):
f_mode = max(10, int(16 * s)) f_mode = max(10, int(16 * s))
f_override = max(10, int(14 * s)) f_override = max(10, int(14 * s))
f_status = max(12, int(20 * s)) f_status = max(12, int(20 * s))
sp = max(6, int(10 * s)) # generic widget spacing
# Proportional size_hint_y weights for the 6 content rows
# title | search | mode | buttons | update-frame | status
_w = [50, 100, 38, 65, 187, 40]
_t = sum(_w)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Build UI # Build UI
@@ -98,12 +76,11 @@ class DatabaseApp(App):
pos_hint={'x': 0, 'y': 0} pos_hint={'x': 0, 'y': 0}
) )
# --- Content container (fills remaining space above numpad) --- # --- Content container: size_hint_y=1 so it fills all space above numpad ---
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=None) content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=1)
content_layout.bind(minimum_height=content_layout.setter('height'))
# Title row: title + Exit button # Title row: title + Exit button
title_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_title, spacing=sp) title_row = BoxLayout(orientation='horizontal', size_hint_y=_w[0]/_t, spacing=sp)
title = Label(text='Database Search & Update', font_size=f_title, bold=True) title = Label(text='Database Search & Update', font_size=f_title, bold=True)
title_row.add_widget(title) title_row.add_widget(title)
exit_btn = Button( exit_btn = Button(
@@ -121,8 +98,8 @@ class DatabaseApp(App):
# Search section (ID + Mass) # Search section (ID + Mass)
search_layout = GridLayout( search_layout = GridLayout(
cols=2, size_hint_y=None, height=h_search, cols=2, size_hint_y=_w[1]/_t,
spacing=sp, row_force_default=True, row_default_height=h_row spacing=sp
) )
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True)) search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
self.id_input = TextInput( self.id_input = TextInput(
@@ -145,7 +122,7 @@ class DatabaseApp(App):
# Mode indicator row # Mode indicator row
self.manual_override = None self.manual_override = None
mode_row = BoxLayout(orientation='horizontal', size_hint_y=None, height=h_mode, spacing=sp) mode_row = BoxLayout(orientation='horizontal', size_hint_y=_w[2]/_t, spacing=sp)
self.mode_label = Label( self.mode_label = Label(
text='Article type detected: PRODUCT', text='Article type detected: PRODUCT',
size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1) size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1)
@@ -160,7 +137,7 @@ class DatabaseApp(App):
content_layout.add_widget(mode_row) content_layout.add_widget(mode_row)
# Action buttons (Add/Update, Reset, Settings) # Action buttons (Add/Update, Reset, Settings)
button_layout = GridLayout(cols=3, size_hint_y=None, height=h_buttons, spacing=sp) button_layout = GridLayout(cols=3, size_hint_y=_w[3]/_t, spacing=sp)
add_update_btn = Button(text='Add/Update', font_size=f_btn, bold=True) add_update_btn = Button(text='Add/Update', font_size=f_btn, bold=True)
add_update_btn.bind(on_press=self.show_update_frame) add_update_btn.bind(on_press=self.show_update_frame)
button_layout.add_widget(add_update_btn) button_layout.add_widget(add_update_btn)
@@ -175,16 +152,15 @@ class DatabaseApp(App):
# Update frame # Update frame
self.update_frame = BoxLayout( self.update_frame = BoxLayout(
orientation='vertical', padding=upd_pad, spacing=upd_spc, orientation='vertical', padding=upd_pad, spacing=upd_spc,
size_hint_y=None, height=h_update size_hint_y=_w[4]/_t
) )
self.update_frame_label = Label( self.update_frame_label = Label(
text='Update Values', size_hint_y=None, height=h_upd_title, text='Update Values', size_hint_y=0.20,
font_size=f_btn, bold=True font_size=f_btn, bold=True
) )
self.update_frame.add_widget(self.update_frame_label) self.update_frame.add_widget(self.update_frame_label)
update_inputs = GridLayout( update_inputs = GridLayout(
cols=2, spacing=sp, size_hint_y=None, height=h_upd_inputs, cols=2, spacing=sp, size_hint_y=0.52
row_force_default=True, row_default_height=h_upd_row
) )
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True)) update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
self.update_id_input = TextInput( self.update_id_input = TextInput(
@@ -198,7 +174,7 @@ class DatabaseApp(App):
self.update_mass_input.bind(focus=self.on_mass_input_focus) self.update_mass_input.bind(focus=self.on_mass_input_focus)
update_inputs.add_widget(self.update_mass_input) update_inputs.add_widget(self.update_mass_input)
self.update_frame.add_widget(update_inputs) self.update_frame.add_widget(update_inputs)
update_buttons = GridLayout(cols=2, size_hint_y=None, height=h_upd_btns, spacing=sp) update_buttons = GridLayout(cols=2, size_hint_y=0.28, spacing=sp)
self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True) self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=f_btn, bold=True)
self.update_confirm_btn.bind(on_press=self.add_update_record) self.update_confirm_btn.bind(on_press=self.add_update_record)
update_buttons.add_widget(self.update_confirm_btn) update_buttons.add_widget(self.update_confirm_btn)
@@ -212,16 +188,12 @@ class DatabaseApp(App):
# Status label # Status label
self.status_label = Label( self.status_label = Label(
text='Ready', size_hint_y=None, height=h_status, text='Ready', size_hint_y=_w[5]/_t,
color=(0, 0.8, 0, 1), font_size=f_status, bold=True color=(0, 0.8, 0, 1), font_size=f_status, bold=True
) )
content_layout.add_widget(self.status_label) content_layout.add_widget(self.status_label)
# Wrap content in an AnchorLayout that fills all space above the numpad main_layout.add_widget(content_layout)
# so the content block is vertically centred regardless of screen size
content_anchor = AnchorLayout(anchor_x='center', anchor_y='center', size_hint_y=1)
content_anchor.add_widget(content_layout)
main_layout.add_widget(content_anchor)
# --- Numeric keypad --- # --- Numeric keypad ---
numpad_wrapper = BoxLayout( numpad_wrapper = BoxLayout(