corally 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
corally/gui/main.py ADDED
@@ -0,0 +1,886 @@
1
+ import tkinter as tk
2
+ from tkinter import ttk, messagebox
3
+ import subprocess
4
+ import threading
5
+ import requests
6
+ from ..core import CalculatorCore, CurrencyConverter, InterestCalculator
7
+ # Backward compatibility aliases
8
+ Rechner = CalculatorCore
9
+ Waerungsrechner = CurrencyConverter
10
+ tageszins = InterestCalculator.calculate_interest
11
+
12
+ class ModernCalculatorGUI:
13
+ def __init__(self, root):
14
+ self.root = root
15
+ self.root.title("Calculator Suite")
16
+ self.root.geometry("800x600")
17
+ self.root.configure(bg='#2c3e50')
18
+
19
+ # Initialize calculator
20
+ self.rechner = Rechner()
21
+
22
+ # Configure style
23
+ self.setup_styles()
24
+
25
+ # Create main interface
26
+ self.create_main_interface()
27
+
28
+ # API server status
29
+ self.api_server_running = False
30
+ self.api_process = None
31
+ self.use_free_api = True # Default to free API
32
+ self.api_port = 8000 # Default port
33
+
34
+ def setup_styles(self):
35
+ """Configure modern styling"""
36
+ style = ttk.Style()
37
+ style.theme_use('clam')
38
+
39
+ # Configure colors
40
+ style.configure('Title.TLabel',
41
+ background='#2c3e50',
42
+ foreground='#ecf0f1',
43
+ font=('Arial', 24, 'bold'))
44
+
45
+ style.configure('Subtitle.TLabel',
46
+ background='#2c3e50',
47
+ foreground='#bdc3c7',
48
+ font=('Arial', 12))
49
+
50
+ style.configure('Modern.TButton',
51
+ background='#3498db',
52
+ foreground='white',
53
+ font=('Arial', 11, 'bold'),
54
+ borderwidth=0,
55
+ focuscolor='none')
56
+
57
+ style.map('Modern.TButton',
58
+ background=[('active', '#2980b9')])
59
+
60
+ style.configure('Success.TButton',
61
+ background='#27ae60',
62
+ foreground='white',
63
+ font=('Arial', 11, 'bold'))
64
+
65
+ style.map('Success.TButton',
66
+ background=[('active', '#229954')])
67
+
68
+ style.configure('Warning.TButton',
69
+ background='#e74c3c',
70
+ foreground='white',
71
+ font=('Arial', 11, 'bold'))
72
+
73
+ style.map('Warning.TButton',
74
+ background=[('active', '#c0392b')])
75
+
76
+ def create_main_interface(self):
77
+ """Create the main interface with tabs"""
78
+ # Title
79
+ title_label = ttk.Label(self.root, text="Calculator Suite", style='Title.TLabel')
80
+ title_label.pack(pady=20)
81
+
82
+ subtitle_label = ttk.Label(self.root, text="Modern Calculator with Multiple Functions", style='Subtitle.TLabel')
83
+ subtitle_label.pack(pady=(0, 20))
84
+
85
+ # Create menu bar
86
+ self.create_menu_bar()
87
+
88
+ # Create notebook for tabs
89
+ self.notebook = ttk.Notebook(self.root)
90
+ self.notebook.pack(fill='both', expand=True, padx=20, pady=20)
91
+
92
+ # Track which tabs have been created
93
+ self.tabs_created = {
94
+ 'basic': False,
95
+ 'interest': False,
96
+ 'api': False
97
+ }
98
+
99
+ # Create placeholder frames for main tabs (excluding currency converter)
100
+ self.tab_frames = {}
101
+ self.create_tab_placeholders()
102
+
103
+ # Currency converter window reference
104
+ self.currency_window = None
105
+
106
+ # Bind tab selection event
107
+ self.notebook.bind('<<NotebookTabChanged>>', self.on_tab_changed)
108
+
109
+ # Create the first tab (Basic Calculator) immediately
110
+ self.create_basic_calculator_tab()
111
+
112
+ def create_basic_calculator_tab(self):
113
+ """Create basic calculator tab content"""
114
+ calc_frame = self.tab_frames['basic']
115
+ self.tabs_created['basic'] = True
116
+
117
+ # Calculator display
118
+ self.calc_display = tk.Text(calc_frame, height=3, width=50, font=('Arial', 14),
119
+ bg='#34495e', fg='#ecf0f1', insertbackground='#ecf0f1')
120
+ self.calc_display.pack(pady=20)
121
+
122
+ # Input frame
123
+ input_frame = ttk.Frame(calc_frame)
124
+ input_frame.pack(pady=10)
125
+
126
+ ttk.Label(input_frame, text="Number 1:").grid(row=0, column=0, padx=5, pady=5)
127
+ self.num1_entry = ttk.Entry(input_frame, font=('Arial', 12))
128
+ self.num1_entry.grid(row=0, column=1, padx=5, pady=5)
129
+
130
+ ttk.Label(input_frame, text="Number 2:").grid(row=1, column=0, padx=5, pady=5)
131
+ self.num2_entry = ttk.Entry(input_frame, font=('Arial', 12))
132
+ self.num2_entry.grid(row=1, column=1, padx=5, pady=5)
133
+
134
+ # Operation buttons
135
+ button_frame = ttk.Frame(calc_frame)
136
+ button_frame.pack(pady=20)
137
+
138
+ operations = [
139
+ ("Addition (+)", lambda: self.calculate('add')),
140
+ ("Subtraction (-)", lambda: self.calculate('subtract')),
141
+ ("Multiplication (ร—)", lambda: self.calculate('multiply')),
142
+ ("Division (รท)", lambda: self.calculate('divide'))
143
+ ]
144
+
145
+ for i, (text, command) in enumerate(operations):
146
+ btn = ttk.Button(button_frame, text=text, command=command, style='Modern.TButton')
147
+ btn.grid(row=i//2, column=i%2, padx=10, pady=5, sticky='ew')
148
+
149
+ # Clear button
150
+ clear_btn = ttk.Button(calc_frame, text="Clear", command=self.clear_calculator, style='Warning.TButton')
151
+ clear_btn.pack(pady=10)
152
+
153
+ def create_currency_converter_window_content(self, parent_window):
154
+ """Create currency converter content in a separate window"""
155
+ # Main frame for the window content
156
+ main_frame = ttk.Frame(parent_window)
157
+ main_frame.pack(fill='both', expand=True, padx=20, pady=20)
158
+
159
+ ttk.Label(main_frame, text="Currency Converter (Static Rates)",
160
+ font=('Arial', 16, 'bold')).pack(pady=20)
161
+
162
+ # Input frame
163
+ input_frame = ttk.Frame(main_frame)
164
+ input_frame.pack(pady=20)
165
+
166
+ ttk.Label(input_frame, text="Amount:").grid(row=0, column=0, padx=5, pady=5)
167
+ self.currency_amount = ttk.Entry(input_frame, font=('Arial', 12))
168
+ self.currency_amount.grid(row=0, column=1, padx=5, pady=5)
169
+
170
+ # Conversion buttons
171
+ conversions = [
172
+ ("EUR โ†’ USD", lambda: self.convert_currency('eur_to_usd')),
173
+ ("USD โ†’ EUR", lambda: self.convert_currency('usd_to_eur')),
174
+ ("EUR โ†’ GBP", lambda: self.convert_currency('eur_to_gbp')),
175
+ ("GBP โ†’ EUR", lambda: self.convert_currency('gbp_to_eur')),
176
+ ("EUR โ†’ YEN", lambda: self.convert_currency('eur_to_yen')),
177
+ ("YEN โ†’ EUR", lambda: self.convert_currency('yen_to_eur'))
178
+ ]
179
+
180
+ button_frame = ttk.Frame(main_frame)
181
+ button_frame.pack(pady=20)
182
+
183
+ for i, (text, command) in enumerate(conversions):
184
+ btn = ttk.Button(button_frame, text=text, command=command, style='Modern.TButton')
185
+ btn.grid(row=i//3, column=i%3, padx=5, pady=5, sticky='ew')
186
+
187
+ # Result display
188
+ self.currency_result = tk.Text(main_frame, height=5, width=60, font=('Arial', 12),
189
+ bg='#34495e', fg='#ecf0f1', insertbackground='#ecf0f1')
190
+ self.currency_result.pack(pady=20)
191
+
192
+ def create_interest_calculator_content(self):
193
+ """Create interest calculator tab content"""
194
+ interest_frame = self.tab_frames['interest']
195
+
196
+ ttk.Label(interest_frame, text="Interest Calculator",
197
+ font=('Arial', 16, 'bold')).pack(pady=20)
198
+
199
+ # Input frame
200
+ input_frame = ttk.Frame(interest_frame)
201
+ input_frame.pack(pady=20)
202
+
203
+ # Capital
204
+ ttk.Label(input_frame, text="Capital (โ‚ฌ):").grid(row=0, column=0, padx=5, pady=5, sticky='w')
205
+ self.capital_entry = ttk.Entry(input_frame, font=('Arial', 12))
206
+ self.capital_entry.grid(row=0, column=1, padx=5, pady=5)
207
+
208
+ # Interest rate
209
+ ttk.Label(input_frame, text="Interest Rate (%):").grid(row=1, column=0, padx=5, pady=5, sticky='w')
210
+ self.rate_entry = ttk.Entry(input_frame, font=('Arial', 12))
211
+ self.rate_entry.grid(row=1, column=1, padx=5, pady=5)
212
+
213
+ # Start date
214
+ ttk.Label(input_frame, text="Start Date (DD.MM.YYYY):").grid(row=2, column=0, padx=5, pady=5, sticky='w')
215
+ self.start_date_entry = ttk.Entry(input_frame, font=('Arial', 12))
216
+ self.start_date_entry.grid(row=2, column=1, padx=5, pady=5)
217
+
218
+ # End date
219
+ ttk.Label(input_frame, text="End Date (DD.MM.YYYY):").grid(row=3, column=0, padx=5, pady=5, sticky='w')
220
+ self.end_date_entry = ttk.Entry(input_frame, font=('Arial', 12))
221
+ self.end_date_entry.grid(row=3, column=1, padx=5, pady=5)
222
+
223
+ # Method
224
+ ttk.Label(input_frame, text="Method:").grid(row=4, column=0, padx=5, pady=5, sticky='w')
225
+ self.method_var = tk.StringVar(value="act/365")
226
+ method_combo = ttk.Combobox(input_frame, textvariable=self.method_var,
227
+ values=["30/360", "act/360", "act/365", "act/act"],
228
+ font=('Arial', 12))
229
+ method_combo.grid(row=4, column=1, padx=5, pady=5)
230
+
231
+ # Calculate button
232
+ calc_btn = ttk.Button(interest_frame, text="Calculate Interest",
233
+ command=self.calculate_interest, style='Success.TButton')
234
+ calc_btn.pack(pady=20)
235
+
236
+ # Result display
237
+ self.interest_result = tk.Text(interest_frame, height=8, width=60, font=('Arial', 12),
238
+ bg='#34495e', fg='#ecf0f1', insertbackground='#ecf0f1')
239
+ self.interest_result.pack(pady=20)
240
+
241
+ def create_api_currency_content(self):
242
+ """Create API currency converter tab content"""
243
+ api_frame = self.tab_frames['api']
244
+
245
+ ttk.Label(api_frame, text="Live Currency Converter",
246
+ font=('Arial', 16, 'bold')).pack(pady=20)
247
+
248
+ # API Selection
249
+ api_selection_frame = ttk.Frame(api_frame)
250
+ api_selection_frame.pack(pady=10)
251
+
252
+ ttk.Label(api_selection_frame, text="API Mode:", font=('Arial', 12, 'bold')).pack(side='left', padx=5)
253
+
254
+ self.api_mode_var = tk.StringVar(value="free")
255
+ free_radio = ttk.Radiobutton(api_selection_frame, text="Free API (No key required)",
256
+ variable=self.api_mode_var, value="free",
257
+ command=self.on_api_mode_change)
258
+ free_radio.pack(side='left', padx=5)
259
+
260
+ paid_radio = ttk.Radiobutton(api_selection_frame, text="Paid API (Requires .env file)",
261
+ variable=self.api_mode_var, value="paid",
262
+ command=self.on_api_mode_change)
263
+ paid_radio.pack(side='left', padx=5)
264
+
265
+ # Server control
266
+ control_frame = ttk.Frame(api_frame)
267
+ control_frame.pack(pady=10)
268
+
269
+ self.server_status_label = ttk.Label(control_frame, text="API Server: Stopped",
270
+ font=('Arial', 12, 'bold'))
271
+ self.server_status_label.pack(pady=5)
272
+
273
+ self.start_server_btn = ttk.Button(control_frame, text="Start API Server",
274
+ command=self.start_api_server, style='Success.TButton')
275
+ self.start_server_btn.pack(side='left', padx=5)
276
+
277
+ self.stop_server_btn = ttk.Button(control_frame, text="Stop API Server",
278
+ command=self.stop_api_server, style='Warning.TButton')
279
+ self.stop_server_btn.pack(side='left', padx=5)
280
+
281
+ # Force stop button (always enabled)
282
+ self.force_stop_btn = ttk.Button(control_frame, text="Force Stop",
283
+ command=self.force_stop_server, style='Danger.TButton')
284
+ self.force_stop_btn.pack(side='left', padx=5)
285
+
286
+ # Conversion interface
287
+ conversion_frame = ttk.Frame(api_frame)
288
+ conversion_frame.pack(pady=20)
289
+
290
+ ttk.Label(conversion_frame, text="From Currency:").grid(row=0, column=0, padx=5, pady=5)
291
+ self.from_currency = ttk.Entry(conversion_frame, font=('Arial', 12))
292
+ self.from_currency.grid(row=0, column=1, padx=5, pady=5)
293
+
294
+ ttk.Label(conversion_frame, text="To Currency:").grid(row=1, column=0, padx=5, pady=5)
295
+ self.to_currency = ttk.Entry(conversion_frame, font=('Arial', 12))
296
+ self.to_currency.grid(row=1, column=1, padx=5, pady=5)
297
+
298
+ ttk.Label(conversion_frame, text="Amount:").grid(row=2, column=0, padx=5, pady=5)
299
+ self.api_amount = ttk.Entry(conversion_frame, font=('Arial', 12))
300
+ self.api_amount.grid(row=2, column=1, padx=5, pady=5)
301
+
302
+ # Conversion and test buttons
303
+ button_frame2 = ttk.Frame(api_frame)
304
+ button_frame2.pack(pady=20)
305
+
306
+ convert_btn = ttk.Button(button_frame2, text="Convert Currency",
307
+ command=self.api_convert_currency, style='Modern.TButton')
308
+ convert_btn.pack(side='left', padx=5)
309
+
310
+ test_btn = ttk.Button(button_frame2, text="Test Output",
311
+ command=self.test_api_output, style='Success.TButton')
312
+ test_btn.pack(side='left', padx=5)
313
+
314
+ clear_btn = ttk.Button(button_frame2, text="Clear Results",
315
+ command=self.clear_api_results, style='Warning.TButton')
316
+ clear_btn.pack(side='left', padx=5)
317
+
318
+ # Result display section
319
+ result_label = ttk.Label(api_frame, text="Conversion Results:", font=('Arial', 12, 'bold'))
320
+ result_label.pack(pady=(10, 5))
321
+
322
+ # Create frame for text widget and scrollbar
323
+ text_frame = ttk.Frame(api_frame)
324
+ text_frame.pack(pady=10, padx=20, fill='both', expand=True)
325
+
326
+ # Create text widget
327
+ self.api_result = tk.Text(text_frame, height=10, width=70, font=('Arial', 11),
328
+ bg='#34495e', fg='#ecf0f1', insertbackground='#ecf0f1',
329
+ wrap=tk.WORD, state=tk.NORMAL, relief='solid', bd=1)
330
+ self.api_result.pack(side='left', fill='both', expand=True)
331
+
332
+ # Add scrollbar
333
+ result_scrollbar = ttk.Scrollbar(text_frame, orient='vertical', command=self.api_result.yview)
334
+ result_scrollbar.pack(side='right', fill='y')
335
+ self.api_result.config(yscrollcommand=result_scrollbar.set)
336
+
337
+ # Add initial text to verify widget is working
338
+ self.api_result.insert(tk.END, "๐Ÿ’ก Live Currency Converter Ready\n")
339
+ self.api_result.insert(tk.END, "1. Select API mode above\n")
340
+ self.api_result.insert(tk.END, "2. Start the API server\n")
341
+ self.api_result.insert(tk.END, "3. Enter currencies and amount\n")
342
+ self.api_result.insert(tk.END, "4. Click 'Convert Currency'\n")
343
+ self.api_result.insert(tk.END, f"{'-'*40}\n")
344
+
345
+ def create_menu_bar(self):
346
+ """Create menu bar with Tools dropdown for Currency Converter"""
347
+ menubar = tk.Menu(self.root)
348
+ self.root.config(menu=menubar)
349
+
350
+ # Tools menu
351
+ tools_menu = tk.Menu(menubar, tearoff=0)
352
+ menubar.add_cascade(label="Tools", menu=tools_menu)
353
+ tools_menu.add_command(label="Currency Converter (Static)", command=self.open_currency_converter)
354
+ tools_menu.add_separator()
355
+ tools_menu.add_command(label="About", command=self.show_about)
356
+
357
+ def open_currency_converter(self):
358
+ """Open Currency Converter in a separate window"""
359
+ if self.currency_window is not None and self.currency_window.winfo_exists():
360
+ # Window already exists, bring it to front
361
+ self.currency_window.lift()
362
+ self.currency_window.focus_force()
363
+ return
364
+
365
+ # Create new currency converter window
366
+ self.currency_window = tk.Toplevel(self.root)
367
+ self.currency_window.title("Currency Converter (Static Rates)")
368
+ self.currency_window.geometry("500x400")
369
+ self.currency_window.resizable(True, True)
370
+
371
+ # Apply dark theme to the window
372
+ self.currency_window.configure(bg='#2c3e50')
373
+
374
+ # Create currency converter content in the new window
375
+ self.create_currency_converter_window_content(self.currency_window)
376
+
377
+ # Handle window closing
378
+ self.currency_window.protocol("WM_DELETE_WINDOW", self.close_currency_converter)
379
+
380
+ def close_currency_converter(self):
381
+ """Close the currency converter window"""
382
+ if self.currency_window:
383
+ self.currency_window.destroy()
384
+ self.currency_window = None
385
+
386
+ def show_about(self):
387
+ """Show about dialog"""
388
+ about_text = """Calculator Suite v2.0
389
+
390
+ Modern calculator with multiple functions:
391
+ โ€ข Basic Calculator with logging
392
+ โ€ข Interest Calculator
393
+ โ€ข Live Currency API
394
+ โ€ข Currency Converter (Static Rates)
395
+
396
+ Developed with Python & tkinter"""
397
+
398
+ from tkinter import messagebox
399
+ messagebox.showinfo("About Calculator Suite", about_text)
400
+
401
+ def create_tab_placeholders(self):
402
+ """Create placeholder frames for main tabs (excluding currency converter)"""
403
+ # Basic Calculator tab
404
+ self.tab_frames['basic'] = ttk.Frame(self.notebook)
405
+ self.notebook.add(self.tab_frames['basic'], text="Basic Calculator")
406
+
407
+ # Interest Calculator tab
408
+ self.tab_frames['interest'] = ttk.Frame(self.notebook)
409
+ self.notebook.add(self.tab_frames['interest'], text="Interest Calculator")
410
+
411
+ # Live Currency API tab
412
+ self.tab_frames['api'] = ttk.Frame(self.notebook)
413
+ self.notebook.add(self.tab_frames['api'], text="Live Currency API")
414
+
415
+ # Add loading labels to empty tabs
416
+ for tab_name, frame in self.tab_frames.items():
417
+ if tab_name != 'basic': # Don't add to basic tab since it loads immediately
418
+ loading_label = ttk.Label(frame, text=f"Loading {tab_name.title()} Calculator...",
419
+ font=('Arial', 14), foreground='gray')
420
+ loading_label.pack(expand=True)
421
+
422
+ def on_tab_changed(self, event=None): # noqa: ARG002
423
+ """Handle tab selection changes - create tab content on first access"""
424
+ selected_tab = self.notebook.index(self.notebook.select())
425
+
426
+ # Map tab indices to tab names (currency converter removed)
427
+ tab_mapping = {0: 'basic', 1: 'interest', 2: 'api'}
428
+ tab_name = tab_mapping.get(selected_tab)
429
+
430
+ if tab_name and not self.tabs_created[tab_name]:
431
+ # Clear the loading label
432
+ for widget in self.tab_frames[tab_name].winfo_children():
433
+ widget.destroy()
434
+
435
+ # Create the actual tab content
436
+ if tab_name == 'interest':
437
+ self.create_interest_calculator_content()
438
+ elif tab_name == 'api':
439
+ self.create_api_currency_content()
440
+
441
+ # Mark as created
442
+ self.tabs_created[tab_name] = True
443
+
444
+ def calculate(self, operation):
445
+ """Perform basic calculator operations"""
446
+ try:
447
+ num1 = float(self.num1_entry.get())
448
+ num2 = float(self.num2_entry.get())
449
+
450
+ if operation == 'add':
451
+ result = self.rechner.add(num1, num2)
452
+ op_symbol = '+'
453
+ elif operation == 'subtract':
454
+ result = self.rechner.subtract(num1, num2)
455
+ op_symbol = '-'
456
+ elif operation == 'multiply':
457
+ result = self.rechner.multiply(num1, num2)
458
+ op_symbol = 'ร—'
459
+ elif operation == 'divide':
460
+ result = self.rechner.divide(num1, num2)
461
+ op_symbol = 'รท'
462
+
463
+ if result is not None:
464
+ calculation_text = f"{num1} {op_symbol} {num2} = {result}\n"
465
+ self.calc_display.insert(tk.END, calculation_text)
466
+ self.calc_display.see(tk.END)
467
+ else:
468
+ messagebox.showerror("Error", "Calculation failed. Check your inputs.")
469
+
470
+ except ValueError:
471
+ messagebox.showerror("Error", "Please enter valid numbers.")
472
+
473
+ def clear_calculator(self):
474
+ """Clear calculator display and inputs"""
475
+ self.calc_display.delete(1.0, tk.END)
476
+ self.num1_entry.delete(0, tk.END)
477
+ self.num2_entry.delete(0, tk.END)
478
+
479
+ def convert_currency(self, conversion_type):
480
+ """Convert currency using static rates"""
481
+ try:
482
+ amount = float(self.currency_amount.get())
483
+
484
+ if conversion_type == 'eur_to_usd':
485
+ result = Waerungsrechner.eur_to_usd(amount)
486
+ text = f"{amount} EUR = {result} USD\n"
487
+ elif conversion_type == 'usd_to_eur':
488
+ result = Waerungsrechner.usd_to_eur(amount)
489
+ text = f"{amount} USD = {result} EUR\n"
490
+ elif conversion_type == 'eur_to_gbp':
491
+ result = Waerungsrechner.eur_to_gbp(amount)
492
+ text = f"{amount} EUR = {result} GBP\n"
493
+ elif conversion_type == 'gbp_to_eur':
494
+ result = Waerungsrechner.gbp_to_eur(amount)
495
+ text = f"{amount} GBP = {result} EUR\n"
496
+ elif conversion_type == 'eur_to_yen':
497
+ result = Waerungsrechner.eur_to_jpy(amount)
498
+ text = f"{amount} EUR = {result} JPY\n"
499
+ elif conversion_type == 'yen_to_eur':
500
+ result = Waerungsrechner.jpy_to_eur(amount)
501
+ text = f"{amount} JPY = {result} EUR\n"
502
+
503
+ if result is not None:
504
+ self.currency_result.insert(tk.END, text)
505
+ self.currency_result.see(tk.END)
506
+ else:
507
+ messagebox.showerror("Error", "Conversion failed.")
508
+
509
+ except ValueError:
510
+ messagebox.showerror("Error", "Please enter a valid amount.")
511
+
512
+ def calculate_interest(self):
513
+ """Calculate interest using the zinsen module"""
514
+ try:
515
+ capital = float(self.capital_entry.get())
516
+ rate = float(self.rate_entry.get())
517
+ start_date = self.start_date_entry.get()
518
+ end_date = self.end_date_entry.get()
519
+ method = self.method_var.get()
520
+
521
+ interest = tageszins(capital, rate, start_date, end_date, method)
522
+
523
+ result_text = f"""
524
+ ๐Ÿ“Š Interest Calculation Result:
525
+ Capital: {capital:,.2f} โ‚ฌ
526
+ Interest Rate: {rate}% per year
527
+ Period: {start_date} to {end_date}
528
+ Method: {method}
529
+ Interest: {interest:,.2f} โ‚ฌ
530
+ Total: {capital + interest:,.2f} โ‚ฌ
531
+ {'-'*40}
532
+ """
533
+
534
+ self.interest_result.insert(tk.END, result_text)
535
+ self.interest_result.see(tk.END)
536
+
537
+ except ValueError as e:
538
+ messagebox.showerror("Error", f"Invalid input: {str(e)}")
539
+ except Exception as e:
540
+ messagebox.showerror("Error", f"Calculation failed: {str(e)}")
541
+
542
+ def on_api_mode_change(self):
543
+ """Handle API mode change"""
544
+ if self.api_server_running:
545
+ messagebox.showinfo("Info", "Please stop the current server before changing API mode.")
546
+ return
547
+
548
+ mode = self.api_mode_var.get()
549
+ if mode == "free":
550
+ self.use_free_api = True
551
+ else:
552
+ self.use_free_api = False
553
+
554
+ def find_available_port(self, start_port=8000):
555
+ """Find an available port starting from start_port"""
556
+ import socket
557
+ for port in range(start_port, start_port + 10):
558
+ try:
559
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
560
+ s.bind(('127.0.0.1', port))
561
+ return port
562
+ except OSError:
563
+ continue
564
+ return None
565
+
566
+ def start_api_server(self):
567
+ """Start the FastAPI server in a separate thread"""
568
+ if not self.api_server_running:
569
+ def run_server():
570
+ try:
571
+ # Find available port
572
+ port = self.find_available_port(8000)
573
+ if not port:
574
+ self.root.after(0, lambda: messagebox.showerror("Error", "No available ports found (8000-8009)"))
575
+ return
576
+
577
+ self.api_port = port
578
+
579
+ # Choose which API to start based on mode
580
+ if self.use_free_api:
581
+ api_module = "api_free:app"
582
+ else:
583
+ api_module = "api:app"
584
+
585
+ self.api_process = subprocess.Popen(
586
+ ["uvicorn", api_module, "--host", "127.0.0.1", "--port", str(port)],
587
+ stdout=subprocess.PIPE,
588
+ stderr=subprocess.PIPE
589
+ )
590
+
591
+ # Wait a moment and check if server started
592
+ import time
593
+ time.sleep(2)
594
+
595
+ # Test if server is responding
596
+ try:
597
+ import requests
598
+ response = requests.get(f"http://127.0.0.1:{port}/", timeout=5)
599
+ if response.status_code == 200:
600
+ self.api_server_running = True
601
+ mode_text = "Free API" if self.use_free_api else "Paid API"
602
+ self.root.after(0, lambda: self.update_server_status(f"Running ({mode_text}) on port {port}"))
603
+ else:
604
+ raise Exception(f"Server not responding (status: {response.status_code})")
605
+ except Exception as e:
606
+ self.api_process.terminate()
607
+ self.root.after(0, lambda err=str(e): messagebox.showerror("Error", f"Server failed to start properly: {err}"))
608
+
609
+ except Exception as error:
610
+ self.root.after(0, lambda err=error: messagebox.showerror("Error", f"Failed to start server: {str(err)}"))
611
+
612
+ threading.Thread(target=run_server, daemon=True).start()
613
+
614
+ def stop_api_server(self):
615
+ """Stop the FastAPI server"""
616
+ try:
617
+ # Show progress
618
+ self.api_result.insert(tk.END, "๐Ÿ›‘ Stopping API server...\n")
619
+ self.api_result.see(tk.END)
620
+ self.root.update()
621
+
622
+ stopped = False
623
+
624
+ # Method 1: Try to stop the process we started
625
+ if self.api_process is not None:
626
+ try:
627
+ self.api_process.terminate()
628
+ self.api_process.wait(timeout=5) # Wait up to 5 seconds
629
+ self.api_process = None
630
+ stopped = True
631
+ self.api_result.insert(tk.END, "โœ… Stopped tracked process\n")
632
+ except Exception as e:
633
+ self.api_result.insert(tk.END, f"โš ๏ธ Could not stop tracked process: {e}\n")
634
+
635
+ # Method 2: Kill any process on the current port (more robust)
636
+ if not stopped:
637
+ try:
638
+ import subprocess
639
+ # Find process on current port
640
+ result = subprocess.run(
641
+ ["netstat", "-ano"],
642
+ capture_output=True,
643
+ text=True,
644
+ timeout=10
645
+ )
646
+
647
+ for line in result.stdout.split('\n'):
648
+ if f':{self.api_port}' in line and ('LISTENING' in line or 'ABHร–REN' in line):
649
+ # Extract PID (last column)
650
+ parts = line.split()
651
+ if parts:
652
+ pid = parts[-1]
653
+ if pid.isdigit():
654
+ try:
655
+ subprocess.run(["taskkill", "/PID", pid, "/F"],
656
+ capture_output=True, timeout=5)
657
+ stopped = True
658
+ self.api_result.insert(tk.END, f"โœ… Killed process {pid} on port {self.api_port}\n")
659
+ break
660
+ except Exception:
661
+ pass
662
+ except Exception as e:
663
+ self.api_result.insert(tk.END, f"โš ๏ธ Port check failed: {e}\n")
664
+
665
+ # Update GUI state
666
+ self.api_server_running = False
667
+ self.api_process = None
668
+
669
+ if stopped:
670
+ self.update_server_status("Stopped")
671
+ self.api_result.insert(tk.END, "โœ… API Server stopped successfully!\n")
672
+ self.api_result.insert(tk.END, f"{'-'*40}\n")
673
+ messagebox.showinfo("Success", "API Server stopped successfully!")
674
+ else:
675
+ self.update_server_status("Unknown")
676
+ self.api_result.insert(tk.END, "โš ๏ธ Could not confirm server was stopped\n")
677
+ self.api_result.insert(tk.END, "๐Ÿ’ก Try 'Force Stop' if server is still running\n")
678
+ self.api_result.insert(tk.END, f"{'-'*40}\n")
679
+ messagebox.showwarning("Warning", "Could not confirm server was stopped. Try 'Force Stop' if needed.")
680
+
681
+ self.api_result.see(tk.END)
682
+
683
+ except Exception as e:
684
+ self.api_result.insert(tk.END, f"โŒ Stop failed: {str(e)}\n")
685
+ self.api_result.see(tk.END)
686
+ messagebox.showerror("Error", f"Failed to stop server: {str(e)}")
687
+
688
+ def force_stop_server(self):
689
+ """Force stop any API server on common ports"""
690
+ try:
691
+ import subprocess
692
+ ports_to_check = [8000, 8001, 8002, 8003]
693
+ stopped_any = False
694
+
695
+ for port in ports_to_check:
696
+ try:
697
+ # Find processes on this port
698
+ result = subprocess.run(
699
+ ["netstat", "-ano"],
700
+ capture_output=True,
701
+ text=True,
702
+ timeout=10
703
+ )
704
+
705
+ for line in result.stdout.split('\n'):
706
+ if f':{port}' in line and ('LISTENING' in line or 'ABHร–REN' in line):
707
+ parts = line.split()
708
+ if parts:
709
+ pid = parts[-1]
710
+ if pid.isdigit():
711
+ try:
712
+ subprocess.run(["taskkill", "/PID", pid, "/F"],
713
+ capture_output=True, timeout=5)
714
+ stopped_any = True
715
+ print(f"Killed process {pid} on port {port}")
716
+ except Exception:
717
+ pass
718
+ except Exception:
719
+ pass
720
+
721
+ # Reset GUI state
722
+ self.api_server_running = False
723
+ self.api_process = None
724
+
725
+ if stopped_any:
726
+ self.update_server_status("Force Stopped")
727
+ messagebox.showinfo("Success", "Force stopped API servers on ports 8000-8003")
728
+ else:
729
+ self.update_server_status("No servers found")
730
+ messagebox.showinfo("Info", "No API servers found running on ports 8000-8003")
731
+
732
+ except Exception as e:
733
+ messagebox.showerror("Error", f"Force stop failed: {str(e)}")
734
+
735
+ def update_server_status(self, status):
736
+ """Update server status label"""
737
+ self.server_status_label.config(text=f"API Server: {status}")
738
+ if "Running" in status:
739
+ self.start_server_btn.config(state='disabled')
740
+ self.stop_server_btn.config(state='normal')
741
+ else:
742
+ self.start_server_btn.config(state='normal')
743
+ # Keep stop button enabled - user might need to force stop
744
+ self.stop_server_btn.config(state='normal')
745
+
746
+ def api_convert_currency(self):
747
+ """Convert currency using the live API"""
748
+ # First check if server is actually reachable
749
+ if not self.check_api_server_health():
750
+ return
751
+
752
+ try:
753
+ from_curr = self.from_currency.get().strip().upper()
754
+ to_curr = self.to_currency.get().strip().upper()
755
+ amount = self.api_amount.get().strip()
756
+
757
+ if not all([from_curr, to_curr, amount]):
758
+ messagebox.showerror("Error", "Please fill in all fields.")
759
+ return
760
+
761
+ # Validate inputs
762
+ try:
763
+ float(amount.replace(",", "."))
764
+ except ValueError:
765
+ messagebox.showerror("Error", "Please enter a valid amount.")
766
+ return
767
+
768
+ if len(from_curr) != 3 or len(to_curr) != 3:
769
+ messagebox.showerror("Error", "Please use 3-letter currency codes (e.g., EUR, USD).")
770
+ return
771
+
772
+ # Show progress
773
+ self.api_result.insert(tk.END, f"๐Ÿ”„ Converting {amount} {from_curr} to {to_curr}...\n")
774
+ self.api_result.see(tk.END)
775
+ self.root.update()
776
+
777
+ # Make API request
778
+ url = f"http://127.0.0.1:{self.api_port}/convert"
779
+ params = {
780
+ "from_currency": from_curr,
781
+ "to_currency": to_curr,
782
+ "amount": amount
783
+ }
784
+
785
+ response = requests.get(url, params=params, timeout=15)
786
+
787
+ if response.status_code == 200:
788
+ data = response.json()
789
+ cached_status = "Cached" if data.get("cached", False) else "Live"
790
+
791
+ result_text = f"""โœ… {cached_status} Currency Conversion:
792
+ {data['amount']} {data['from']} = {data['result']:.2f} {data['to']}
793
+ Exchange Rate: {data['info']['rate']:.4f}
794
+ {'-'*40}
795
+ """
796
+ self.api_result.insert(tk.END, result_text)
797
+ self.api_result.see(tk.END)
798
+ else:
799
+ error_msg = f"โŒ API Error {response.status_code}: {response.text}\n{'-'*40}\n"
800
+ self.api_result.insert(tk.END, error_msg)
801
+ self.api_result.see(tk.END)
802
+
803
+ except requests.exceptions.ConnectionError:
804
+ self.api_result.insert(tk.END, "โŒ Connection Error: API server not reachable\n")
805
+ self.api_result.insert(tk.END, "๐Ÿ’ก Try restarting the API server\n{'-'*40}\n")
806
+ self.api_result.see(tk.END)
807
+ self.api_server_running = False
808
+ self.update_server_status("Stopped")
809
+ except requests.exceptions.Timeout:
810
+ self.api_result.insert(tk.END, "โŒ Timeout Error: API server too slow\n{'-'*40}\n")
811
+ self.api_result.see(tk.END)
812
+ except requests.exceptions.RequestException as e:
813
+ self.api_result.insert(tk.END, f"โŒ Network Error: {str(e)}\n{'-'*40}\n")
814
+ self.api_result.see(tk.END)
815
+ except Exception as e:
816
+ self.api_result.insert(tk.END, f"โŒ Unexpected Error: {str(e)}\n{'-'*40}\n")
817
+ self.api_result.see(tk.END)
818
+
819
+ def check_api_server_health(self):
820
+ """Check if API server is actually running and reachable"""
821
+ try:
822
+ response = requests.get(f"http://127.0.0.1:{self.api_port}/", timeout=5)
823
+ if response.status_code == 200:
824
+ return True
825
+ else:
826
+ self.api_result.insert(tk.END, f"โŒ Server health check failed: {response.status_code}\n")
827
+ self.api_result.see(tk.END)
828
+ return False
829
+ except requests.exceptions.ConnectionError:
830
+ self.api_result.insert(tk.END, "โŒ API server not running. Please start it first.\n")
831
+ self.api_result.see(tk.END)
832
+ self.api_server_running = False
833
+ self.update_server_status("Stopped")
834
+ return False
835
+ except Exception as e:
836
+ self.api_result.insert(tk.END, f"โŒ Server check failed: {str(e)}\n")
837
+ self.api_result.see(tk.END)
838
+ return False
839
+
840
+ def test_api_output(self):
841
+ """Test the API output text widget"""
842
+ try:
843
+ self.api_result.insert(tk.END, "\n๐Ÿงช Testing output widget...\n")
844
+ self.api_result.insert(tk.END, "โœ… Text widget is working!\n")
845
+
846
+ # Test formatted output like real API response
847
+ test_result = f"""โœ… Test Currency Conversion:
848
+ 100.0 EUR = 116.00 USD
849
+ Exchange Rate: 1.1600
850
+ {'-'*40}
851
+ """
852
+ self.api_result.insert(tk.END, test_result)
853
+ self.api_result.see(tk.END)
854
+
855
+ messagebox.showinfo("Success", "Output widget is working correctly!")
856
+
857
+ except Exception as e:
858
+ messagebox.showerror("Error", f"Output widget test failed: {str(e)}")
859
+
860
+ def clear_api_results(self):
861
+ """Clear the API results text widget"""
862
+ try:
863
+ self.api_result.delete(1.0, tk.END)
864
+ self.api_result.insert(tk.END, "๐Ÿ’ก Results cleared. Ready for new conversions.\n")
865
+ self.api_result.insert(tk.END, f"{'-'*40}\n")
866
+ except Exception as e:
867
+ messagebox.showerror("Error", f"Failed to clear results: {str(e)}")
868
+
869
+
870
+ def main():
871
+ """Main function to run the application"""
872
+ root = tk.Tk()
873
+ app = ModernCalculatorGUI(root)
874
+
875
+ # Handle window closing
876
+ def on_closing():
877
+ if hasattr(app, 'api_server_running') and app.api_server_running:
878
+ app.stop_api_server()
879
+ root.destroy()
880
+
881
+ root.protocol("WM_DELETE_WINDOW", on_closing)
882
+ root.mainloop()
883
+
884
+
885
+ if __name__ == "__main__":
886
+ main()