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/__init__.py +36 -0
- corally/api/__init__.py +8 -0
- corally/api/free_server.py +259 -0
- corally/api/server.py +190 -0
- corally/cli/__init__.py +9 -0
- corally/cli/calculator.py +59 -0
- corally/cli/currency.py +65 -0
- corally/cli/main.py +208 -0
- corally/core/__init__.py +7 -0
- corally/core/calculator.py +392 -0
- corally/gui/__init__.py +8 -0
- corally/gui/launcher.py +138 -0
- corally/gui/main.py +886 -0
- corally-1.0.0.dist-info/METADATA +192 -0
- corally-1.0.0.dist-info/RECORD +19 -0
- corally-1.0.0.dist-info/WHEEL +5 -0
- corally-1.0.0.dist-info/entry_points.txt +5 -0
- corally-1.0.0.dist-info/licenses/LICENSE +21 -0
- corally-1.0.0.dist-info/top_level.txt +1 -0
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()
|