Files
db_interface/main.py
scheianu c912bac2dc feat: persistent DB host config, async DB ops, Exit button, test connection
- Save/load DB host from config.json (works both in dev and built .exe)
- All DB operations moved to background threads to prevent UI blocking
- Connection timeout set to 5s to avoid long hangs on unreachable host
- Added Test Connection button to Settings popup
- Added red Exit button fixed to bottom-right corner of main screen
- Updated DatabaseApp.spec with full Kivy deps (sdl2, glew, angle) and hidden imports
- PyInstaller-aware base path using sys.frozen for config.json persistence
2026-04-01 21:00:45 +03:00

446 lines
18 KiB
Python

import threading
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.anchorlayout import AnchorLayout
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()
def build(self):
# Set window to fullscreen
Window.fullscreen = 'auto'
# Root float layout so we can overlay the exit button
root = FloatLayout()
# Main layout with better spacing for fullscreen
main_layout = BoxLayout(orientation='vertical', padding=40, spacing=20,
size_hint=(1, 1), pos_hint={'x': 0, 'y': 0})
# Top spacer for vertical centering
main_layout.add_widget(Label(size_hint_y=0.15))
# Content container - centered
content_layout = BoxLayout(orientation='vertical', spacing=30, size_hint_y=None)
content_layout.bind(minimum_height=content_layout.setter('height'))
# Title
title = Label(text='Database Search & Update', font_size=28, bold=True, size_hint_y=None, height=50)
content_layout.add_widget(title)
# Search section
search_layout = GridLayout(cols=2, size_hint_y=None, height=100, spacing=15, row_force_default=True, row_default_height=45)
search_layout.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=20, bold=True))
self.id_input = TextInput(
multiline=False,
size_hint_x=0.75,
hint_text='Enter ID and press Enter to search (max 20 chars)',
readonly=False,
font_size=20,
padding=[10, 10]
)
self.id_input.bind(on_text_validate=self.search_record)
search_layout.add_widget(self.id_input)
search_layout.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=20, bold=True))
self.mass_input = TextInput(
multiline=False,
size_hint_x=0.75,
hint_text='Mass value (read-only)',
readonly=True,
font_size=20,
padding=[10, 10]
)
search_layout.add_widget(self.mass_input)
content_layout.add_widget(search_layout)
# Button section - larger buttons (3 columns now)
button_layout = GridLayout(cols=3, size_hint_y=None, height=70, spacing=15)
add_update_btn = Button(text='Add/Update', font_size=22, 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=22, bold=True)
reset_btn.bind(on_press=self.reset_values)
button_layout.add_widget(reset_btn)
settings_btn = Button(text='Settings', font_size=22, bold=True)
settings_btn.bind(on_press=self.show_settings)
button_layout.add_widget(settings_btn)
content_layout.add_widget(button_layout)
# Extra spacing between buttons and update frame
content_layout.add_widget(Label(size_hint_y=None, height=40))
# Update frame (initially disabled)
self.update_frame = BoxLayout(orientation='vertical', padding=15, spacing=15, size_hint_y=None, height=200)
self.update_frame_label = Label(text='Update Values', size_hint_y=None, height=40, font_size=22, bold=True)
self.update_frame.add_widget(self.update_frame_label)
update_inputs = GridLayout(cols=2, spacing=15, size_hint_y=None, height=100, row_force_default=True, row_default_height=45)
update_inputs.add_widget(Label(text='ID:', size_hint_x=0.25, font_size=20, bold=True))
self.update_id_input = TextInput(multiline=False, size_hint_x=0.75, readonly=True, font_size=20, padding=[10, 10])
update_inputs.add_widget(self.update_id_input)
update_inputs.add_widget(Label(text='Mass:', size_hint_x=0.25, font_size=20, bold=True))
self.update_mass_input = TextInput(multiline=False, size_hint_x=0.75, readonly=True, font_size=20, padding=[10, 10])
update_inputs.add_widget(self.update_mass_input)
self.update_frame.add_widget(update_inputs)
# Add update and delete buttons in same row
update_buttons = GridLayout(cols=2, size_hint_y=None, height=60, spacing=15)
self.update_confirm_btn = Button(text='Confirm Add/Update', disabled=True, font_size=22, 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=22, 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)
# Initially disable update frame
self.set_update_frame_enabled(False)
# Status label - larger and more prominent
self.status_label = Label(
text='Ready',
size_hint_y=None,
height=50,
color=(0, 0.8, 0, 1),
font_size=22,
bold=True
)
content_layout.add_widget(self.status_label)
main_layout.add_widget(content_layout)
# Bottom spacer for vertical centering
main_layout.add_widget(Label(size_hint_y=0.15))
root.add_widget(main_layout)
# Exit button — fixed to the bottom-right corner
exit_btn = Button(
text='Exit',
font_size=20,
bold=True,
background_color=(0.85, 0.1, 0.1, 1),
color=(1, 1, 1, 1),
size_hint=(None, None),
size=(120, 55),
pos_hint={'right': 1, 'y': 0}
)
exit_btn.bind(on_press=lambda x: self.stop())
root.add_widget(exit_btn)
# 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 set_update_frame_enabled(self, enabled):
self.update_id_input.readonly = not enabled
self.update_mass_input.readonly = not enabled
self.update_confirm_btn.disabled = not enabled
self.delete_btn.disabled = not enabled
def show_update_frame(self, instance):
# If no value in search, copy from search fields
record_id = self.id_input.text.strip()
mass_text = self.mass_input.text.strip()
self.set_update_frame_enabled(True)
# If mass field is empty, just clear update frame
if not record_id:
self.update_id_input.text = ''
self.update_mass_input.text = ''
return
self.update_id_input.text = record_id
self.update_mass_input.text = mass_text
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)
def _do():
try:
record = self.db_manager.search_by_id(record_id)
def _update(dt):
if record:
self.mass_input.text = str(record[1])
self.show_status(f"Found: {record[0]} = {record[1]}")
self.highlight_record(record_id)
else:
self.show_status(f"ID '{record_id}' not found in database", error=True)
self.mass_input.text = ""
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.update_id_input.text.strip()
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}")
self.update_id_input.text = ""
self.update_mass_input.text = ""
self.set_update_frame_enabled(False)
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.update_id_input.text.strip()
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.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()