- main.py: _pending_record_id locks resolved DB key at Add/Update time; show original barcode in update frame; auto-focus mass field on open; clear all fields and return focus to ID input after confirm/reset - database_manager.py: buffered=True cursors on all SELECTs; no fetchall() after DML; replace ON DUPLICATE KEY UPDATE VALUES() with explicit UPDATE then INSERT fallback; add app_actions.log with structured per-action entries; purge_old_action_logs(30) on startup - dist/DatabaseApp.exe: rebuilt single-file Windows binary (30.9 MB) - remove unused files: README, WINDOWS_README, run_app.sh, setup_database.sh, setup_user.sql, test_database.py, sept.csv"
639 lines
27 KiB
Python
639 lines
27 KiB
Python
import threading
|
||
from kivy.config import Config
|
||
Config.set('kivy', 'keyboard_mode', 'system')
|
||
from kivy.app import App
|
||
from kivy.uix.boxlayout import BoxLayout
|
||
from kivy.uix.floatlayout import FloatLayout
|
||
from kivy.uix.gridlayout import GridLayout
|
||
from kivy.uix.label import Label
|
||
from kivy.uix.textinput import TextInput
|
||
from kivy.uix.button import Button
|
||
from kivy.uix.scrollview import ScrollView
|
||
from kivy.uix.popup import Popup
|
||
from kivy.clock import Clock
|
||
from kivy.core.window import Window
|
||
from database_manager import DatabaseManager
|
||
|
||
class DatabaseApp(App):
|
||
def __init__(self, **kwargs):
|
||
super().__init__(**kwargs)
|
||
self.db_manager = DatabaseManager()
|
||
self.active_numpad_input = None
|
||
self._pending_record_id = None # resolved (trimmed) ID locked at show_update_frame time
|
||
|
||
def build(self):
|
||
# Set window to fullscreen first so Window.height reflects the screen
|
||
Window.fullscreen = 'auto'
|
||
|
||
# ------------------------------------------------------------------
|
||
# Responsive sizing: derive dimensions from the actual screen height
|
||
# ------------------------------------------------------------------
|
||
wh = Window.height
|
||
|
||
# Scale factor (1.0 at 1080p, down to 0.65 on small screens)
|
||
s = max(0.65, min(1.0, wh / 1080.0))
|
||
|
||
# Padding / spacing (all scaled)
|
||
pad_v = max(8, int(20 * s))
|
||
pad_h = max(8, int(30 * s))
|
||
m_spacing = max(4, int(10 * s))
|
||
c_spacing = max(4, int(12 * s))
|
||
sp = max(6, int(10 * s))
|
||
upd_pad = max(4, int(8 * s))
|
||
upd_spc = max(4, int(8 * s))
|
||
np_pad_v = 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_numpad_gr = h_numpad_wr - h_enter_btn - 2 * np_pad_v - np_spc
|
||
|
||
# Font sizes (scaled)
|
||
f_title = max(14, int(26 * s))
|
||
f_normal = max(11, int(18 * s))
|
||
f_btn = max(12, int(20 * s))
|
||
f_numpad = max(15, int(26 * s))
|
||
f_enter = max(14, int(24 * s))
|
||
f_mode = max(10, int(16 * s))
|
||
f_override = max(10, int(14 * s))
|
||
f_status = max(12, int(20 * s))
|
||
|
||
# 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
|
||
# ------------------------------------------------------------------
|
||
root = FloatLayout()
|
||
|
||
main_layout = BoxLayout(
|
||
orientation='vertical',
|
||
padding=[pad_h, pad_v, pad_h, pad_v],
|
||
spacing=m_spacing,
|
||
size_hint=(1, 1),
|
||
pos_hint={'x': 0, 'y': 0}
|
||
)
|
||
|
||
# --- Content container: size_hint_y=1 so it fills all space above numpad ---
|
||
content_layout = BoxLayout(orientation='vertical', spacing=c_spacing, size_hint_y=1)
|
||
|
||
# Title row: title + Exit button
|
||
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_row.add_widget(title)
|
||
exit_btn = Button(
|
||
text='Exit',
|
||
font_size=max(11, int(16 * s)),
|
||
bold=True,
|
||
background_color=(0.85, 0.1, 0.1, 1),
|
||
color=(1, 1, 1, 1),
|
||
size_hint_x=None,
|
||
width=max(70, int(100 * s))
|
||
)
|
||
exit_btn.bind(on_press=lambda x: self.stop())
|
||
title_row.add_widget(exit_btn)
|
||
content_layout.add_widget(title_row)
|
||
|
||
# Search section (ID + Mass)
|
||
search_layout = GridLayout(
|
||
cols=2, size_hint_y=_w[1]/_t,
|
||
spacing=sp
|
||
)
|
||
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||
self.id_input = TextInput(
|
||
multiline=False, size_hint_x=0.75,
|
||
hint_text='Enter ID (max 20 chars)',
|
||
readonly=False, font_size=f_normal, padding=[7, 7]
|
||
)
|
||
self.id_input.bind(on_text_validate=self.search_record)
|
||
self.id_input.bind(on_text=self.update_mode_indicator)
|
||
self.id_input.bind(focus=self.on_id_input_focus)
|
||
search_layout.add_widget(self.id_input)
|
||
search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||
# Mass input + last-update label share the 0.75 right side equally
|
||
mass_row = BoxLayout(orientation='horizontal', size_hint_x=0.75, spacing=sp)
|
||
self.mass_input = TextInput(
|
||
multiline=False, size_hint_x=0.5,
|
||
hint_text='Mass (read-only)',
|
||
readonly=True, font_size=f_normal, padding=[7, 7]
|
||
)
|
||
mass_row.add_widget(self.mass_input)
|
||
self.last_update_label = Label(
|
||
text='Last update: never',
|
||
size_hint_x=0.5, font_size=max(9, int(13 * s)),
|
||
bold=False, color=(0.7, 0.7, 0.7, 1),
|
||
halign='left', valign='middle'
|
||
)
|
||
self.last_update_label.bind(size=self.last_update_label.setter('text_size'))
|
||
mass_row.add_widget(self.last_update_label)
|
||
search_layout.add_widget(mass_row)
|
||
content_layout.add_widget(search_layout)
|
||
|
||
# Mode indicator row
|
||
self.manual_override = None
|
||
mode_row = BoxLayout(orientation='horizontal', size_hint_y=_w[2]/_t, spacing=sp)
|
||
self.mode_label = Label(
|
||
text='Article type detected: PRODUCT',
|
||
size_hint_x=0.75, font_size=f_mode, bold=True, color=(0.4, 0.8, 1, 1)
|
||
)
|
||
mode_row.add_widget(self.mode_label)
|
||
self.override_btn = Button(
|
||
text='Override type', size_hint_x=0.25,
|
||
font_size=f_override, bold=True, background_color=(0.3, 0.3, 0.3, 1)
|
||
)
|
||
self.override_btn.bind(on_press=self.toggle_override)
|
||
mode_row.add_widget(self.override_btn)
|
||
content_layout.add_widget(mode_row)
|
||
|
||
# Action buttons (Add/Update, Reset, Settings)
|
||
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.bind(on_press=self.show_update_frame)
|
||
button_layout.add_widget(add_update_btn)
|
||
reset_btn = Button(text='Reset Values', font_size=f_btn, bold=True)
|
||
reset_btn.bind(on_press=self.reset_values)
|
||
button_layout.add_widget(reset_btn)
|
||
settings_btn = Button(text='Settings', font_size=f_btn, bold=True)
|
||
settings_btn.bind(on_press=self.show_settings)
|
||
button_layout.add_widget(settings_btn)
|
||
content_layout.add_widget(button_layout)
|
||
|
||
# Update frame
|
||
self.update_frame = BoxLayout(
|
||
orientation='vertical', padding=upd_pad, spacing=upd_spc,
|
||
size_hint_y=_w[4]/_t
|
||
)
|
||
self.update_frame_label = Label(
|
||
text='Update Values', size_hint_y=0.20,
|
||
font_size=f_btn, bold=True
|
||
)
|
||
self.update_frame.add_widget(self.update_frame_label)
|
||
update_inputs = GridLayout(
|
||
cols=2, spacing=sp, size_hint_y=0.52
|
||
)
|
||
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||
self.update_id_input = TextInput(
|
||
multiline=False, size_hint_x=0.75, readonly=True, font_size=f_normal, padding=[7, 7]
|
||
)
|
||
update_inputs.add_widget(self.update_id_input)
|
||
update_inputs.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=f_normal, bold=True))
|
||
self.update_mass_input = TextInput(
|
||
multiline=False, size_hint_x=0.75, readonly=True, font_size=f_normal, padding=[7, 7]
|
||
)
|
||
self.update_mass_input.bind(focus=self.on_mass_input_focus)
|
||
update_inputs.add_widget(self.update_mass_input)
|
||
self.update_frame.add_widget(update_inputs)
|
||
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.bind(on_press=self.add_update_record)
|
||
update_buttons.add_widget(self.update_confirm_btn)
|
||
self.delete_btn = Button(text='Delete', disabled=True, font_size=f_btn, bold=True)
|
||
self.delete_btn.bind(on_press=self.delete_record)
|
||
update_buttons.add_widget(self.delete_btn)
|
||
self.update_frame.add_widget(update_buttons)
|
||
content_layout.add_widget(self.update_frame)
|
||
|
||
self.set_update_frame_enabled(False)
|
||
|
||
# Status label
|
||
self.status_label = Label(
|
||
text='Ready', size_hint_y=_w[5]/_t,
|
||
color=(0, 0.8, 0, 1), font_size=f_status, bold=True
|
||
)
|
||
content_layout.add_widget(self.status_label)
|
||
|
||
main_layout.add_widget(content_layout)
|
||
|
||
# --- Numeric keypad ---
|
||
numpad_wrapper = BoxLayout(
|
||
orientation='vertical', size_hint_y=None, height=h_numpad_wr,
|
||
spacing=np_spc, padding=[pad_h, np_pad_v, pad_h, np_pad_v]
|
||
)
|
||
numpad = GridLayout(cols=3, size_hint_y=None, height=h_numpad_gr, spacing=max(3, int(6 * s)))
|
||
for digit in ['1', '2', '3', '4', '5', '6', '7', '8', '9']:
|
||
btn = Button(text=digit, font_size=f_numpad, bold=True)
|
||
btn.bind(on_press=self.numpad_press)
|
||
numpad.add_widget(btn)
|
||
dot_btn = Button(text='.', font_size=f_numpad, bold=True, background_color=(0.25, 0.25, 0.45, 1))
|
||
dot_btn.bind(on_press=self.numpad_press)
|
||
numpad.add_widget(dot_btn)
|
||
zero_btn = Button(text='0', font_size=f_numpad, bold=True)
|
||
zero_btn.bind(on_press=self.numpad_press)
|
||
numpad.add_widget(zero_btn)
|
||
back_btn = Button(text='⌫', font_size=f_numpad, bold=True, background_color=(0.5, 0.3, 0.1, 1))
|
||
back_btn.bind(on_press=self.numpad_backspace)
|
||
numpad.add_widget(back_btn)
|
||
numpad_wrapper.add_widget(numpad)
|
||
enter_btn = Button(
|
||
text='Enter', font_size=f_enter, bold=True,
|
||
background_color=(0.1, 0.55, 0.1, 1), size_hint_y=None, height=h_enter_btn
|
||
)
|
||
enter_btn.bind(on_press=self.numpad_enter)
|
||
numpad_wrapper.add_widget(enter_btn)
|
||
main_layout.add_widget(numpad_wrapper)
|
||
|
||
root.add_widget(main_layout)
|
||
|
||
# Init DB and load data in background so the UI appears immediately
|
||
def _init_db(dt):
|
||
def _do():
|
||
self.db_manager.init_database()
|
||
Clock.schedule_once(lambda dt2: self.refresh_data(None))
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
Clock.schedule_once(_init_db, 0.1)
|
||
|
||
return root
|
||
def on_id_input_focus(self, instance, focused):
|
||
"""Track active numpad target when ID field gains focus."""
|
||
if focused:
|
||
self.active_numpad_input = self.id_input
|
||
|
||
def on_mass_input_focus(self, instance, focused):
|
||
"""Track active numpad target when mass field gains focus."""
|
||
if focused:
|
||
self.active_numpad_input = self.update_mass_input
|
||
|
||
def _refocus_active(self):
|
||
"""Return keyboard focus to the active field so scanner input keeps working."""
|
||
target = self.active_numpad_input if self.active_numpad_input else self.id_input
|
||
if not target.readonly:
|
||
Clock.schedule_once(lambda dt: setattr(target, 'focus', True), 0.05)
|
||
|
||
def numpad_enter(self, instance):
|
||
"""Enter key: trigger search if ID field is active, then refocus."""
|
||
if self.active_numpad_input is self.id_input or self.active_numpad_input is None:
|
||
self.search_record(instance)
|
||
self._refocus_active()
|
||
|
||
def numpad_press(self, instance):
|
||
"""Append a digit/dot to the active input field, then refocus for scanner."""
|
||
target = self.active_numpad_input if self.active_numpad_input else self.id_input
|
||
if not target.readonly:
|
||
target.text += instance.text
|
||
self._refocus_active()
|
||
|
||
def numpad_backspace(self, instance):
|
||
"""Remove the last character from the active input field, then refocus for scanner."""
|
||
target = self.active_numpad_input if self.active_numpad_input else self.id_input
|
||
if not target.readonly:
|
||
target.text = target.text[:-1]
|
||
self._refocus_active()
|
||
|
||
def set_update_frame_enabled(self, enabled):
|
||
self.update_id_input.readonly = True # ID is always readonly – always set from search
|
||
self.update_mass_input.readonly = not enabled
|
||
self.update_confirm_btn.disabled = not enabled
|
||
self.delete_btn.disabled = not enabled
|
||
|
||
def _resolve_id(self, raw_id: str) -> str:
|
||
"""Resolve ID using manual override if set, otherwise auto-detect."""
|
||
mode = self.manual_override if self.manual_override else (
|
||
'BOX' if (len(raw_id) == 8 and raw_id.isdigit()) else 'PRODUCT'
|
||
)
|
||
if mode == 'BOX':
|
||
return raw_id.lstrip('0') or '0'
|
||
return raw_id
|
||
|
||
def toggle_override(self, instance):
|
||
"""Manually flip the detected type between BOX and PRODUCT."""
|
||
current_auto = 'BOX' if (len(self.id_input.text.strip()) == 8 and self.id_input.text.strip().isdigit()) else 'PRODUCT'
|
||
# Determine current effective mode
|
||
effective = self.manual_override if self.manual_override else current_auto
|
||
# Flip it
|
||
self.manual_override = 'PRODUCT' if effective == 'BOX' else 'BOX'
|
||
self._apply_mode_label(self.manual_override, is_override=True)
|
||
|
||
def update_mode_indicator(self, instance, value):
|
||
"""Auto-detect mode on each keystroke; resets any manual override."""
|
||
self.manual_override = None
|
||
text = value.strip()
|
||
mode = 'BOX' if (len(text) == 8 and text.isdigit()) else 'PRODUCT'
|
||
self._apply_mode_label(mode, is_override=False)
|
||
|
||
def _apply_mode_label(self, mode, is_override):
|
||
"""Update the mode label and override button appearance."""
|
||
prefix = '[Manual] ' if is_override else ''
|
||
if mode == 'BOX':
|
||
self.mode_label.text = f'{prefix}Article type detected: BOX'
|
||
self.mode_label.color = (1, 0.75, 0, 1)
|
||
else:
|
||
self.mode_label.text = f'{prefix}Article type detected: PRODUCT'
|
||
self.mode_label.color = (0.4, 0.8, 1, 1)
|
||
self.override_btn.background_color = (0.6, 0.2, 0.6, 1) if is_override else (0.3, 0.3, 0.3, 1)
|
||
|
||
def show_update_frame(self, instance):
|
||
record_id = self.id_input.text.strip()
|
||
mass_text = self.mass_input.text.strip()
|
||
self.set_update_frame_enabled(True)
|
||
if not record_id:
|
||
self.update_id_input.text = ''
|
||
self.update_mass_input.text = ''
|
||
self._pending_record_id = None
|
||
return
|
||
# Lock in the resolved (trimmed) DB id now; display original scan for the operator
|
||
self._pending_record_id = self._resolve_id(record_id)
|
||
self.update_id_input.text = record_id # show original barcode value
|
||
self.update_mass_input.text = mass_text
|
||
# Direct numpad and keyboard focus to mass field so operator can immediately enter new mass
|
||
self.active_numpad_input = self.update_mass_input
|
||
Clock.schedule_once(lambda dt: setattr(self.update_mass_input, 'focus', True), 0.05)
|
||
|
||
def search_record(self, instance):
|
||
record_id = self.id_input.text.strip()
|
||
if not record_id:
|
||
self.show_status("Please enter an ID to search", error=True)
|
||
return
|
||
|
||
if len(record_id) > 20:
|
||
self.show_status("ID must be 20 characters or less", error=True)
|
||
return
|
||
|
||
self.show_status("Searching...", error=False)
|
||
|
||
resolved_id = self._resolve_id(record_id)
|
||
def _do():
|
||
try:
|
||
record = self.db_manager.search_by_id(resolved_id)
|
||
def _update(dt):
|
||
if record:
|
||
self.mass_input.text = str(record[1])
|
||
t_update = record[2] if len(record) > 2 else None
|
||
if t_update:
|
||
self.last_update_label.text = f'Last update: {t_update.strftime("%d/%m/%Y %H:%M")}'
|
||
else:
|
||
self.last_update_label.text = 'Last update: never'
|
||
self.show_status(f"Found: {record[0]} = {record[1]}")
|
||
self.highlight_record(resolved_id)
|
||
else:
|
||
self.mass_input.text = ""
|
||
self.last_update_label.text = 'Last update: never'
|
||
self.show_status(f"ID '{record_id}' not found in database", error=True)
|
||
Clock.schedule_once(_update)
|
||
except Exception as e:
|
||
err = str(e)
|
||
Clock.schedule_once(lambda dt: self.show_status(f"Search error: {err}", error=True))
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
|
||
def add_update_record(self, instance):
|
||
"""Add or update a record from the update frame."""
|
||
record_id = self._pending_record_id
|
||
mass_text = self.update_mass_input.text.strip()
|
||
if not record_id or not mass_text:
|
||
self.show_status("Please enter both ID and mass in update frame", error=True)
|
||
return
|
||
if len(record_id) > 20:
|
||
self.show_status("ID must be 20 characters or less", error=True)
|
||
return
|
||
try:
|
||
mass = float(mass_text)
|
||
except ValueError:
|
||
self.show_status("Mass must be a valid number", error=True)
|
||
return
|
||
self.show_status("Saving...", error=False)
|
||
|
||
def _do():
|
||
try:
|
||
success = self.db_manager.add_or_update_record(record_id, mass)
|
||
def _update(dt):
|
||
if success:
|
||
self.show_status(f"Successfully added/updated: {record_id} = {mass}")
|
||
# Clear all fields and return focus to ID input
|
||
self.update_id_input.text = ""
|
||
self.update_mass_input.text = ""
|
||
self.id_input.text = ""
|
||
self.mass_input.text = ""
|
||
self.last_update_label.text = 'Last update: never'
|
||
self._pending_record_id = None
|
||
self.set_update_frame_enabled(False)
|
||
self.active_numpad_input = self.id_input
|
||
Clock.schedule_once(lambda dt2: setattr(self.id_input, 'focus', True), 0.05)
|
||
self.refresh_data(None)
|
||
else:
|
||
self.show_status("Failed to add/update record", error=True)
|
||
Clock.schedule_once(_update)
|
||
except Exception as e:
|
||
err = str(e)
|
||
Clock.schedule_once(lambda dt: self.show_status(f"Save error: {err}", error=True))
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
|
||
def delete_record(self, instance):
|
||
"""Delete a record using the update frame fields."""
|
||
record_id = self._pending_record_id
|
||
if not record_id:
|
||
self.show_status("Please enter an ID in the update fields to delete", error=True)
|
||
return
|
||
# Confirm deletion
|
||
self.show_confirmation_popup(
|
||
f"Are you sure you want to delete ID '{record_id}'?",
|
||
lambda: self.confirm_delete(record_id)
|
||
)
|
||
|
||
def confirm_delete(self, record_id):
|
||
"""Confirm and execute deletion."""
|
||
self.show_status("Deleting...", error=False)
|
||
|
||
def _do():
|
||
try:
|
||
success = self.db_manager.delete_record(record_id)
|
||
def _update(dt):
|
||
if success:
|
||
self.show_status(f"Successfully deleted: {record_id}")
|
||
self.clear_fields()
|
||
self.update_id_input.text = ""
|
||
self.update_mass_input.text = ""
|
||
self.set_update_frame_enabled(False)
|
||
self.refresh_data(None)
|
||
else:
|
||
self.show_status(f"Failed to delete ID '{record_id}' (not found)", error=True)
|
||
Clock.schedule_once(_update)
|
||
except Exception as e:
|
||
err = str(e)
|
||
Clock.schedule_once(lambda dt: self.show_status(f"Delete error: {err}", error=True))
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
|
||
def clear_fields(self):
|
||
"""Clear the ID and mass fields."""
|
||
self.id_input.text = ""
|
||
self.mass_input.text = ""
|
||
|
||
def reset_values(self, instance):
|
||
"""Reset/clear the first ID and mass fields and set focus on ID field."""
|
||
self.id_input.text = ""
|
||
self.mass_input.text = ""
|
||
self.last_update_label.text = 'Last update: never'
|
||
self.update_id_input.text = ""
|
||
self.update_mass_input.text = ""
|
||
self._pending_record_id = None
|
||
self.set_update_frame_enabled(False)
|
||
self.active_numpad_input = self.id_input
|
||
self.id_input.focus = True
|
||
self.show_status("Fields cleared", error=False)
|
||
|
||
def refresh_data(self, instance):
|
||
"""Refresh the data display (runs in a background thread)."""
|
||
def _do():
|
||
try:
|
||
records = self.db_manager.read_all_data()
|
||
count = len(records)
|
||
Clock.schedule_once(lambda dt: self.show_status(f"Data refreshed - {count} records found"))
|
||
except Exception as e:
|
||
err = str(e)
|
||
Clock.schedule_once(lambda dt: self.show_status(f"Error refreshing data: {err}", error=True))
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
|
||
def highlight_record(self, record_id):
|
||
"""Highlight a specific record in the display."""
|
||
# Since we removed the data display, just show a status message
|
||
pass
|
||
|
||
def show_status(self, message, error=False):
|
||
"""Show status message."""
|
||
self.status_label.text = message
|
||
if error:
|
||
self.status_label.color = (1, 0.2, 0.2, 1) # Red for errors
|
||
else:
|
||
self.status_label.color = (0, 0.8, 0, 1) # Green for success
|
||
|
||
# Clear status after 5 seconds
|
||
Clock.schedule_once(lambda dt: self.clear_status(), 5)
|
||
|
||
def clear_status(self):
|
||
"""Clear the status message."""
|
||
self.status_label.text = "Ready"
|
||
self.status_label.color = (0, 0.8, 0, 1)
|
||
|
||
def show_settings(self, instance):
|
||
"""Show settings popup for database configuration."""
|
||
content = BoxLayout(orientation='vertical', spacing=15, padding=20)
|
||
|
||
# Title
|
||
title_label = Label(text='Database Server Settings', font_size=24, bold=True, size_hint_y=None, height=40)
|
||
content.add_widget(title_label)
|
||
|
||
# IP address input
|
||
ip_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=60, spacing=10)
|
||
ip_layout.add_widget(Label(text='Server IP Address:', font_size=20, bold=True, size_hint_x=0.4))
|
||
ip_input = TextInput(
|
||
text=self.db_manager.host,
|
||
multiline=False,
|
||
font_size=20,
|
||
size_hint_x=0.6,
|
||
padding=[10, 10]
|
||
)
|
||
ip_layout.add_widget(ip_input)
|
||
content.add_widget(ip_layout)
|
||
|
||
# Info label
|
||
info_label = Label(
|
||
text='Enter the IP address or hostname of the database server.\nOther connection settings remain unchanged.',
|
||
font_size=16,
|
||
size_hint_y=None,
|
||
height=60
|
||
)
|
||
content.add_widget(info_label)
|
||
|
||
# Test connection button + result label
|
||
test_btn = Button(text='Test Connection', font_size=20, bold=True, size_hint_y=None, height=55)
|
||
content.add_widget(test_btn)
|
||
|
||
test_result_label = Label(
|
||
text='',
|
||
font_size=17,
|
||
size_hint_y=None,
|
||
height=40,
|
||
bold=True
|
||
)
|
||
content.add_widget(test_result_label)
|
||
|
||
# Buttons
|
||
buttons = BoxLayout(size_hint_y=None, height=60, spacing=15)
|
||
|
||
save_btn = Button(text='Save', font_size=20, bold=True)
|
||
cancel_btn = Button(text='Cancel', font_size=20, bold=True)
|
||
|
||
buttons.add_widget(save_btn)
|
||
buttons.add_widget(cancel_btn)
|
||
content.add_widget(buttons)
|
||
|
||
popup = Popup(
|
||
title='Settings',
|
||
content=content,
|
||
size_hint=(0.6, 0.65)
|
||
)
|
||
|
||
def save_settings():
|
||
new_host = ip_input.text.strip()
|
||
if new_host:
|
||
self.db_manager.host = new_host
|
||
self.db_manager.save_host(new_host)
|
||
# Reconnect with new settings
|
||
if self.db_manager.connection:
|
||
try:
|
||
self.db_manager.connection.close()
|
||
except:
|
||
pass
|
||
self.db_manager.connection = None
|
||
self.show_status(f"Database server updated to: {new_host}")
|
||
popup.dismiss()
|
||
else:
|
||
self.show_status("Please enter a valid IP address", error=True)
|
||
|
||
def test_connection(instance):
|
||
host = ip_input.text.strip()
|
||
if not host:
|
||
test_result_label.text = 'Please enter a host first.'
|
||
test_result_label.color = (1, 0.6, 0, 1)
|
||
return
|
||
test_result_label.text = 'Testing...'
|
||
test_result_label.color = (1, 1, 1, 1)
|
||
|
||
def _do():
|
||
success, message = self.db_manager.test_connection(host)
|
||
def _update(dt):
|
||
if success:
|
||
test_result_label.text = f'OK: {message}'
|
||
test_result_label.color = (0, 0.85, 0, 1)
|
||
else:
|
||
test_result_label.text = f'Failed: {message}'
|
||
test_result_label.color = (1, 0.2, 0.2, 1)
|
||
Clock.schedule_once(_update)
|
||
threading.Thread(target=_do, daemon=True).start()
|
||
|
||
test_btn.bind(on_press=test_connection)
|
||
save_btn.bind(on_press=lambda x: save_settings())
|
||
cancel_btn.bind(on_press=popup.dismiss)
|
||
|
||
popup.open()
|
||
|
||
def show_confirmation_popup(self, message, confirm_callback):
|
||
"""Show a confirmation popup."""
|
||
content = BoxLayout(orientation='vertical', spacing=10, padding=10)
|
||
|
||
label = Label(text=message, text_size=(300, None), halign='center')
|
||
content.add_widget(label)
|
||
|
||
buttons = BoxLayout(size_hint_y=None, height=50, spacing=10)
|
||
|
||
yes_btn = Button(text='Yes')
|
||
no_btn = Button(text='No')
|
||
|
||
buttons.add_widget(yes_btn)
|
||
buttons.add_widget(no_btn)
|
||
content.add_widget(buttons)
|
||
|
||
popup = Popup(
|
||
title='Confirm Action',
|
||
content=content,
|
||
size_hint=(0.8, 0.4)
|
||
)
|
||
|
||
yes_btn.bind(on_press=lambda x: (confirm_callback(), popup.dismiss()))
|
||
no_btn.bind(on_press=popup.dismiss)
|
||
|
||
popup.open()
|
||
|
||
if __name__ == '__main__':
|
||
DatabaseApp().run() |