Skip to content

Commit 35c467e

Browse files
authored
Create CurrencyTool.py
1 parent 423d8bd commit 35c467e

1 file changed

Lines changed: 331 additions & 0 deletions

File tree

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
import sys
2+
import os
3+
import json
4+
import threading
5+
import tkinter as tk
6+
from tkinter import ttk, messagebox
7+
import sv_ttk
8+
import requests
9+
10+
# =========================
11+
# Helpers
12+
# =========================
13+
def resource_path(file_name):
14+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
15+
return os.path.join(base_path, file_name)
16+
17+
DATA_FILE = resource_path("rates_live.json")
18+
19+
# =========================
20+
# App Setup
21+
# =========================
22+
root = tk.Tk()
23+
root.title("CurrencyTool Pro")
24+
root.geometry("980x620")
25+
root.minsize(900, 550)
26+
27+
sv_ttk.set_theme("light")
28+
29+
# =========================
30+
# Globals
31+
# =========================
32+
amount_var = tk.DoubleVar(value=100.0)
33+
from_var = tk.StringVar(value="USD")
34+
to_var = tk.StringVar(value="EUR")
35+
result_var = tk.StringVar(value="—")
36+
status_var = tk.StringVar(value="Ready")
37+
mode_var = tk.StringVar(value="Offline")
38+
39+
# Default fallback rates
40+
RATES = {
41+
"USD": 1.0,
42+
"EUR": 0.92,
43+
"GBP": 0.79,
44+
"JPY": 157.0,
45+
"AUD": 1.52,
46+
"CAD": 1.36,
47+
"CHF": 0.88,
48+
"CNY": 7.18
49+
}
50+
51+
# =========================
52+
# Persistence
53+
# =========================
54+
def load_rates():
55+
if os.path.exists(DATA_FILE):
56+
try:
57+
with open(DATA_FILE, "r", encoding="utf-8") as f:
58+
data = json.load(f)
59+
RATES.clear()
60+
RATES.update(data)
61+
RATES["USD"] = 1.0
62+
mode_var.set("Online")
63+
except Exception:
64+
pass
65+
66+
def save_rates():
67+
with open(DATA_FILE, "w", encoding="utf-8") as f:
68+
json.dump(RATES, f, indent=2)
69+
70+
# =========================
71+
# Status
72+
# =========================
73+
def set_status(msg):
74+
status_var.set(msg)
75+
root.update_idletasks()
76+
77+
# =========================
78+
# Live Rate Fetch
79+
# =========================
80+
def fetch_live_rates():
81+
update_btn.config(state="disabled")
82+
set_status("🌐 Fetching live rates...")
83+
try:
84+
r = requests.get("https://open.er-api.com/v6/latest/USD", timeout=6)
85+
data = r.json()
86+
87+
if data.get("result") != "success":
88+
raise RuntimeError
89+
90+
RATES.clear()
91+
RATES.update(data["rates"])
92+
RATES["USD"] = 1.0
93+
94+
save_rates()
95+
refresh_currency_lists()
96+
97+
mode_var.set("Online")
98+
set_status("🌐 Live rates updated & saved")
99+
100+
except Exception:
101+
messagebox.showwarning(
102+
"Live Update Failed",
103+
"Unable to fetch live rates.\nUsing offline data."
104+
)
105+
set_status("Offline mode")
106+
107+
finally:
108+
update_btn.config(state="normal")
109+
110+
# =========================
111+
# Conversion
112+
# =========================
113+
def convert_currency():
114+
try:
115+
amount = amount_var.get()
116+
f, t = from_var.get(), to_var.get()
117+
118+
usd = amount / RATES[f]
119+
result = usd * RATES[t]
120+
121+
result_var.set(f"{result:,.4f} {t}")
122+
set_status(f"Converted ({mode_var.get()})")
123+
124+
except Exception:
125+
messagebox.showerror("Error", "Invalid amount or currency.")
126+
127+
def swap_currencies():
128+
f, t = from_var.get(), to_var.get()
129+
from_var.set(t)
130+
to_var.set(f)
131+
132+
# =========================
133+
# Rate Editor
134+
# =========================
135+
def open_rate_editor():
136+
editor = tk.Toplevel(root)
137+
editor.title("✏️ Manual Rate Override (USD Base)")
138+
139+
# Center the window
140+
w, h = 420, 520
141+
ws = root.winfo_screenwidth()
142+
hs = root.winfo_screenheight()
143+
x = (ws // 2) - (w // 2)
144+
y = (hs // 2) - (h // 2)
145+
editor.geometry(f"{w}x{h}+{x}+{y}")
146+
editor.transient(root)
147+
editor.grab_set()
148+
149+
frame = ttk.Frame(editor, padding=16)
150+
frame.pack(fill="both", expand=True)
151+
152+
ttk.Label(
153+
frame,
154+
text="Manual Rate Override",
155+
font=("Segoe UI", 14, "bold")
156+
).pack(anchor="w")
157+
158+
ttk.Label(
159+
frame,
160+
text="Saving switches app to Offline mode",
161+
foreground="#666"
162+
).pack(anchor="w", pady=(2, 10))
163+
164+
# Scrollable frame
165+
canvas = tk.Canvas(frame)
166+
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
167+
scrollable_frame = ttk.Frame(canvas)
168+
169+
scrollable_frame.bind(
170+
"<Configure>",
171+
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
172+
)
173+
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
174+
canvas.configure(yscrollcommand=scrollbar.set)
175+
176+
canvas.pack(side="left", fill="both", expand=True)
177+
scrollbar.pack(side="right", fill="y")
178+
179+
entries = {}
180+
181+
for c in sorted(RATES.keys()):
182+
row = ttk.Frame(scrollable_frame)
183+
row.pack(fill="x", pady=2)
184+
185+
ttk.Label(row, text=c, width=6).pack(side="left")
186+
187+
v = tk.DoubleVar(value=RATES[c])
188+
ttk.Entry(row, textvariable=v, width=12).pack(side="left", padx=4)
189+
entries[c] = v
190+
191+
def make_save(currency):
192+
return lambda: save_single_rate(currency)
193+
194+
ttk.Button(
195+
row,
196+
text="💾",
197+
width=3,
198+
command=make_save(c),
199+
style="Action.TButton"
200+
).pack(side="right")
201+
202+
def save_single_rate(currency):
203+
try:
204+
RATES[currency] = float(entries[currency].get())
205+
save_rates() # Save to JSON
206+
refresh_currency_lists()
207+
mode_var.set("Offline")
208+
set_status(f"💾 {currency} rate saved")
209+
except Exception:
210+
messagebox.showerror("Invalid Input", "Rate must be numeric.")
211+
212+
213+
# =========================
214+
# UI Refresh
215+
# =========================
216+
def refresh_currency_lists():
217+
cur = sorted(RATES.keys())
218+
from_combo["values"] = cur
219+
to_combo["values"] = cur
220+
221+
# =========================
222+
# Styles
223+
# =========================
224+
style = ttk.Style()
225+
style.configure("Title.TLabel", font=("Segoe UI", 24, "bold"))
226+
style.configure("Subtitle.TLabel", font=("Segoe UI", 11))
227+
style.configure("Action.TButton", font=("Segoe UI", 11, "bold"), padding=10)
228+
style.configure("Result.TLabel", font=("Segoe UI", 18, "bold"))
229+
230+
# =========================
231+
# Layout
232+
# =========================
233+
root.columnconfigure(0, weight=1)
234+
root.rowconfigure(1, weight=1)
235+
236+
# ----- Header -----
237+
header = ttk.Frame(root, padding=(24, 16))
238+
header.grid(row=0, column=0, sticky="ew")
239+
header.columnconfigure(1, weight=1)
240+
241+
ttk.Label(header, text="CurrencyTool Pro", style="Title.TLabel").grid(row=0, column=0, sticky="w")
242+
ttk.Label(
243+
header,
244+
text="Live + Offline currency converter with persistence",
245+
style="Subtitle.TLabel"
246+
).grid(row=1, column=0, sticky="w", pady=(2, 0))
247+
248+
controls = ttk.Frame(header)
249+
controls.grid(row=0, column=1, rowspan=2, sticky="e")
250+
251+
# 🌐 Update Live Rates button
252+
update_btn = ttk.Button(
253+
controls,
254+
text="🌐 Update Live Rates",
255+
command=lambda: threading.Thread(target=fetch_live_rates, daemon=True).start(),
256+
style="Action.TButton"
257+
)
258+
update_btn.pack(side="right", padx=6)
259+
260+
# ✏️ Manual Override button
261+
manual_btn = ttk.Button(
262+
controls,
263+
text="✏️ Manual Override",
264+
command=open_rate_editor,
265+
style="Action.TButton"
266+
)
267+
manual_btn.pack(side="right", padx=6)
268+
269+
# ----- Main Converter Card -----
270+
main = ttk.Frame(root, padding=(24, 0, 24, 16))
271+
main.grid(row=1, column=0, sticky="nsew")
272+
main.columnconfigure(0, weight=1)
273+
274+
card = ttk.LabelFrame(main, text="Converter", padding=20)
275+
card.grid(row=0, column=0, sticky="ew")
276+
for i in range(4):
277+
card.columnconfigure(i, weight=1)
278+
279+
# Amount
280+
ttk.Label(card, text="Amount").grid(row=0, column=0, sticky="w", pady=(0, 4))
281+
ttk.Entry(card, textvariable=amount_var, font=("Segoe UI", 11)).grid(row=1, column=0, sticky="ew", padx=(0, 10))
282+
283+
# From currency
284+
ttk.Label(card, text="From").grid(row=0, column=1, sticky="w", pady=(0, 4))
285+
from_combo = ttk.Combobox(card, textvariable=from_var, state="readonly", font=("Segoe UI", 11))
286+
from_combo.grid(row=1, column=1, sticky="ew", padx=(0, 10))
287+
288+
# To currency
289+
ttk.Label(card, text="To").grid(row=0, column=2, sticky="w", pady=(0, 4))
290+
to_combo = ttk.Combobox(card, textvariable=to_var, state="readonly", font=("Segoe UI", 11))
291+
to_combo.grid(row=1, column=2, sticky="ew", padx=(0, 10))
292+
293+
# Swap button
294+
ttk.Button(card, text="⇄", command=swap_currencies, style="Action.TButton").grid(row=1, column=3, sticky="ew")
295+
296+
# Result
297+
ttk.Label(card, text="Result", style="Subtitle.TLabel").grid(
298+
row=2, column=0, columnspan=4, pady=(20, 4), sticky="w"
299+
)
300+
ttk.Label(card, textvariable=result_var, style="Result.TLabel", font=("Segoe UI", 14, "bold")).grid(
301+
row=3, column=0, columnspan=4, sticky="w"
302+
)
303+
304+
# ----- Actions -----
305+
actions = ttk.Frame(main, padding=(0, 0, 0, 12))
306+
actions.grid(row=1, column=0, pady=16, sticky="ew")
307+
actions.columnconfigure(0, weight=1)
308+
309+
ttk.Button(
310+
actions,
311+
text="Convert",
312+
command=convert_currency,
313+
style="Action.TButton"
314+
).pack(side="left")
315+
316+
ttk.Label(actions, textvariable=mode_var, foreground="#666", font=("Segoe UI", 10)).pack(side="right")
317+
318+
# ----- Status -----
319+
status = ttk.Label(root, textvariable=status_var, anchor="w", padding=6)
320+
status.grid(row=2, column=0, sticky="ew")
321+
322+
# =========================
323+
# Init
324+
# =========================
325+
load_rates()
326+
refresh_currency_lists()
327+
328+
# =========================
329+
# Run
330+
# =========================
331+
root.mainloop()

0 commit comments

Comments
 (0)