Skip to content

Commit c7dc156

Browse files
authored
Add files via upload
1 parent fcfc607 commit c7dc156

16 files changed

Lines changed: 1584 additions & 0 deletions

‎windows/README.md‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# ChandWidget — Windows (PySide6)
2+
3+
نسخه‌ی ویندوزی اپ اندروید Chand، به‌عنوان یک کارت شناور همیشه روی صفحه (widget) که قیمت ۳ نماد رو نشون می‌ده. کلیک روی کارت، پنجره‌ی اصلی برنامه رو باز می‌کنه که همه‌ی نمادها رو با قیمت لحظه‌ای نشون می‌ده و می‌تونی انتخاب کنی کدوم ۳ تا تو ویجت باشن.
4+
5+
این پوشه یک ساب‌پروژه‌ی مستقل پایتونی/PySide6 هست، کنار پروژه‌ی اصلی اندروید (Kotlin/Gradle) تو همین ریپو.
6+
7+
## اجرا (توسعه/تست)
8+
9+
```bash
10+
pip install -r requirements.txt
11+
python main.py
12+
```
13+
14+
روی ویندوز باید همین دو خط رو تو PowerShell یا CMD (با Python 3.10+ نصب‌شده) اجرا کنی.
15+
16+
## چیزی که می‌بینی
17+
18+
- **ویجت شناور**: بدون کادر، همیشه روی صفحه، گوشه‌ی بالا-راست دسکتاپ. با موس قابل جابجاییه (کلیک و بکش)؛ موقعیتش ذخیره می‌شه.
19+
- **کلیک ساده** روی ویجت → پنجره‌ی اصلی باز می‌شه.
20+
- **راست‌کلیک** روی ویجت → منو: به‌روزرسانی، باز کردن برنامه، همیشه روی صفحه، اجرا هنگام روشن شدن ویندوز، خروج.
21+
- **آیکون تو System Tray** (کنار ساعت ویندوز) → کلیک تک برای نمایش/مخفی کردن ویجت، راست‌کلیک برای منو کامل.
22+
- **پنجره‌ی اصلی**: بالا سه‌تا combo box برای انتخاب نماد هر ردیف ویجت، پایین‌ش لیست کامل همه‌ی نمادها با قیمت زنده و جستجو.
23+
24+
## ساخت یک فایل .exe مستقل (روی ویندوز)
25+
26+
این کار حتماً باید **روی خود ویندوز** انجام بشه — PyInstaller باینری مخصوص همون سیستم‌عاملی که روش اجرا می‌شه رو تولید می‌کنه، نه کراس‌کامپایل.
27+
28+
ساده‌ترین راه:
29+
30+
```powershell
31+
build.bat
32+
```
33+
34+
یا دستی:
35+
36+
```powershell
37+
pip install -r requirements.txt pyinstaller
38+
pyinstaller build.spec
39+
```
40+
41+
فایل خروجی تو `dist\ChandWidget.exe` قرار می‌گیره. همون فایل رو هرجا بخوای کپی کن و اجرا کن — نیازی به نصب پایتون روی سیستم مقصد نیست.
42+
43+
آیکون برنامه (تو تری، تسک‌بار و پنجره‌ی اصلی) از `chandwidget/resources/icon.ico` خونده می‌شه و `build.spec` هم به‌عنوان `datas` و هم به‌عنوان آیکون exe به PyInstaller معرفی‌ش می‌کنه، پس هم فایل exe خودش آیکون داره و هم برنامه در زمان اجرا (چه از سورس، چه از exe فریز‌شده) پیداش می‌کنه.
44+
45+
> این ریپو یک GitHub Actions workflow هم داره (`.github/workflows/windows-build.yml`) که همین مراحل رو خودکار روی هر push به `windows/**` یا هر تگ نسخه اجرا می‌کنه و exe رو به‌عنوان artifact (و روی تگ، به‌عنوان Release asset) منتشر می‌کنه — نیازی نیست حتماً خودت روی ویندوز build کنی.
46+
47+
## اجرا خودکار هنگام روشن شدن ویندوز
48+
49+
از منوی راست‌کلیک ویجت گزینه‌ی «اجرا هنگام روشن شدن ویندوز» رو بزن. این یک مقدار تو ثبت مسیر زیر ست می‌کنه (نیازی به دسترسی ادمین نداره):
50+
51+
```
52+
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
53+
```
54+
55+
⚠️ نکته: مسیری که ثبت می‌شه همون `sys.executable` هست. اگه با `python main.py` اجرا کنی، این یعنی مسیر `python.exe` ثبت می‌شه که درست کار نمی‌کنه. این گزینه رو فقط **بعد از ساخت exe و اجرا از همون exe** فعال کن، وگرنه دستی از ثبت (Registry Editor یا PowerShell) پاکش کن.
56+
57+
## ساختار پروژه
58+
59+
```
60+
windows/
61+
main.py ← نقطه‌ی ورود
62+
build.spec ← فایل PyInstaller
63+
build.bat ← اسکریپت خودکار ساخت exe
64+
requirements.txt
65+
chandwidget/
66+
registry.py ← عیناً پورت‌شده از PriceItem.kt/PriceRegistry.kt (همون کلیدها، همون ترتیب)
67+
formatter.py ← عیناً پورت‌شده از Formatter.kt (فرمت قیمت/تغییر، ارقام فارسی، shortLabel)
68+
api.py ← عیناً پورت‌شده از TgjuApi.kt (همون endpointها) + فال‌بک SSL برای شبکه‌های فیلترشده
69+
settings.py ← معادل Prefs.kt، ولی به‌جای SharedPreferences یک JSON تو %APPDATA%\ChandWidget\
70+
refresh.py ← گرفتن قیمت‌ها تو ترد جدا، بدون فریز شدن UI
71+
widget_window.py ← خود کارت شناور
72+
main_window.py ← پنجره‌ی اصلی با لیست کامل و انتخاب‌گر ۳ ردیف
73+
app.py ← اتصال همه‌چیز به هم + آیکون Tray
74+
resources/
75+
icon.ico
76+
icon.png
77+
```
78+
79+
## نکات مهم / محدودیت‌های شناخته‌شده (عیناً از کد اندروید به ارث رسیده)
80+
81+
- کلیدهای XRP، Tron، Dogecoin تو `api.py` هم علامت‌گذاری شده بودن که حدسی‌ان و روی نمونه‌ی واقعی JSON تست نشدن. اگه اینجا هم قیمت این سه‌تا «—» موند، همون مشکل API روی ویندوز هم هست، نه باگ پورت.
82+
- تتر (USDT) از `call4.tgju.org` با یک پارامتر `rev` ثابت خونده می‌شه؛ اگه TGJU اون پارامتر رو عوض کنه، این یکی می‌خوابه — دقیقاً همون ریسکی که تو اندروید هم بود.
83+
- رفرش پیش‌فرض هر ۶۰ ثانیه‌ست (`Settings.get_refresh_seconds`)؛ برای تغییرش فعلاً باید تو `settings.json` مقدار `refresh_seconds` رو دستی عوض کنی — اگه بخوای از پنجره‌ی اصلی هم قابل تنظیم باشه بگو اضافه‌ش می‌کنم.

‎windows/build.bat‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@echo off
2+
REM Run this on Windows, inside this folder (where build.spec is).
3+
4+
where py >nul 2>nul
5+
if errorlevel 1 (
6+
echo Python not found. Install it from python.org and check "Add to PATH".
7+
pause
8+
exit /b 1
9+
)
10+
11+
if not exist venv (
12+
py -m venv venv
13+
)
14+
15+
call venv\Scripts\activate.bat
16+
pip install --upgrade pip
17+
pip install -r requirements.txt
18+
pip install pyinstaller
19+
20+
pyinstaller build.spec
21+
22+
echo.
23+
echo Done. The exe is at dist\ChandWidget.exe
24+
pause

‎windows/build.spec‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
# Build with: pyinstaller build.spec
3+
# Output: dist\ChandWidget.exe
4+
5+
block_cipher = None
6+
7+
a = Analysis(
8+
['main.py'],
9+
pathex=[],
10+
binaries=[],
11+
datas=[
12+
('chandwidget/resources/icon.ico', 'chandwidget/resources'),
13+
('chandwidget/resources/icon.png', 'chandwidget/resources'),
14+
],
15+
hiddenimports=[],
16+
hookspath=[],
17+
hooksconfig={},
18+
runtime_hooks=[],
19+
excludes=[],
20+
noarchive=False,
21+
)
22+
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
23+
24+
exe = EXE(
25+
pyz,
26+
a.scripts,
27+
a.binaries,
28+
a.zipfiles,
29+
a.datas,
30+
[],
31+
name='ChandWidget',
32+
debug=False,
33+
bootloader_ignore_signals=False,
34+
strip=False,
35+
upx=True,
36+
upx_exclude=[],
37+
runtime_tmpdir=None,
38+
console=False, # windowed app, no console popup
39+
disable_windowed_traceback=False,
40+
argv_emulation=False,
41+
target_arch=None,
42+
codesign_identity=None,
43+
entitlements_file=None,
44+
icon='chandwidget/resources/icon.ico',
45+
)

‎windows/chandwidget/__init__.py‎

Whitespace-only changes.

‎windows/chandwidget/api.py‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
"""
2+
Direct port of TgjuApi.java from the Android app.
3+
4+
Non-crypto (currencies, gold, coins):
5+
GET https://api.tgju.org/v1/market/tmp?keys=KEY
6+
response.indicators[].{ name, p (Rial), d (Rial), dt ("high"|"low") }
7+
Exception: "ons" (gold ounce) is already quoted in USD, not Rial.
8+
9+
Crypto BTC/ETH/BNB/SOL via widget endpoint:
10+
GET https://api.tgju.org/v1/widget/tmp?keys=398096,398097,398115,535605
11+
12+
Crypto LTC/BCH/EOS/XRP/TRX/DOGE fallback:
13+
GET https://api.tgju.org/v1/market/indicator/summary-table-data/KEY
14+
15+
Tether (USDT), in Toman like every other currency:
16+
GET https://call4.tgju.org/ajax.json?rev=...
17+
response.current["crypto-tether-irr"].p (Rial)
18+
"""
19+
import logging
20+
from dataclasses import dataclass
21+
from typing import Optional
22+
23+
import requests
24+
import urllib3
25+
26+
from . import registry as R
27+
28+
# Only fires on the verify=False retry path above; the primary request is
29+
# always verified normally.
30+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
31+
32+
log = logging.getLogger("tgju_api")
33+
34+
TIMEOUT = 12 # seconds
35+
36+
TMP_URL = "https://api.tgju.org/v1/market/tmp?keys="
37+
WIDGET_URL = "https://api.tgju.org/v1/widget/tmp?keys=398096,398097,398115,535605"
38+
SUMMARY_URL = "https://api.tgju.org/v1/market/indicator/summary-table-data/"
39+
CALL4_URL = (
40+
"https://call4.tgju.org/ajax.json?rev="
41+
"NUInYHDaLqVjbIse8P1gUgsBjJv0zV4MhEX9NwN3U0tYFAZwZ8iYWMHqm5dN"
42+
)
43+
44+
USD_TMP_KEYS = {R.KEY_GOLD_OUNCE}
45+
WIDGET_KEYS = {R.KEY_BTC, R.KEY_ETH, R.KEY_BNB, R.KEY_SOL}
46+
SUMMARY_KEYS = {R.KEY_LTC, R.KEY_BCH, R.KEY_EOS, R.KEY_XRP, R.KEY_TRX, R.KEY_DOGE}
47+
48+
_HEADERS = {"User-Agent": "ChandWidget-Win/1.0", "Accept": "application/json"}
49+
50+
51+
@dataclass
52+
class PriceData:
53+
key: str
54+
price: float
55+
change: float
56+
57+
58+
def _parse_double(s) -> float:
59+
try:
60+
return float(str(s).replace(",", "").strip())
61+
except (ValueError, TypeError):
62+
return 0.0
63+
64+
65+
def _http_get_json(url: str):
66+
try:
67+
resp = requests.get(url, headers=_HEADERS, timeout=TIMEOUT)
68+
if resp.status_code != 200:
69+
return None
70+
return resp.json()
71+
except requests.exceptions.SSLError:
72+
# Same fallback used in the Tehran Market Terminal project: on some
73+
# networks in Iran, DPI/SSL interference breaks cert verification
74+
# even though the connection itself works. Retry once without
75+
# verification rather than failing outright.
76+
try:
77+
resp = requests.get(url, headers=_HEADERS, timeout=TIMEOUT, verify=False)
78+
if resp.status_code != 200:
79+
return None
80+
return resp.json()
81+
except Exception as e:
82+
log.error("httpGet retry (no verify) failed: %s (%s)", url, e)
83+
return None
84+
except Exception as e:
85+
log.error("httpGet error: %s (%s)", url, e)
86+
return None
87+
88+
89+
def fetch(key: str) -> Optional[PriceData]:
90+
try:
91+
if key == R.KEY_USDT:
92+
return _fetch_tether_irr()
93+
if key in WIDGET_KEYS:
94+
return _fetch_from_widget(key)
95+
if key in SUMMARY_KEYS:
96+
return _fetch_from_summary(key)
97+
return _fetch_from_tmp(key)
98+
except Exception as e:
99+
log.error("fetch error for %s: %s", key, e)
100+
return None
101+
102+
103+
def _fetch_tether_irr() -> Optional[PriceData]:
104+
data = _http_get_json(CALL4_URL)
105+
if not data:
106+
return None
107+
current = data.get("current") or {}
108+
ind = current.get("crypto-tether-irr")
109+
if not ind:
110+
log.warning("crypto-tether-irr not found in call4 feed")
111+
return None
112+
113+
price = _parse_double(ind.get("p", "0")) / 10.0
114+
change = _parse_double(ind.get("d", "0")) / 10.0
115+
dt = ind.get("dt", "")
116+
change = -abs(change) if dt == "low" else abs(change)
117+
return PriceData(R.KEY_USDT, price, change)
118+
119+
120+
def _fetch_from_tmp(key: str) -> Optional[PriceData]:
121+
data = _http_get_json(TMP_URL + key)
122+
if not data:
123+
return None
124+
indicators = data.get("response", {}).get("indicators", [])
125+
for ind in indicators:
126+
if ind.get("name", "") != key:
127+
continue
128+
price = _parse_double(ind.get("p", "0"))
129+
change = _parse_double(ind.get("d", "0"))
130+
dt = ind.get("dt", "")
131+
change = -abs(change) if dt == "low" else abs(change)
132+
if key not in USD_TMP_KEYS:
133+
price /= 10.0
134+
change /= 10.0
135+
return PriceData(key, price, change)
136+
log.warning("Key not found in tmp: %s", key)
137+
return None
138+
139+
140+
def _fetch_from_widget(key: str) -> Optional[PriceData]:
141+
data = _http_get_json(WIDGET_URL)
142+
if data:
143+
indicators = data.get("response", {}).get("indicators", [])
144+
for ind in indicators:
145+
if ind.get("name", "") != key:
146+
continue
147+
price = _parse_double(ind.get("p", "0"))
148+
change = _parse_double(ind.get("d", "0"))
149+
dt = ind.get("dt", "")
150+
change = -abs(change) if dt == "low" else abs(change)
151+
return PriceData(key, price, change)
152+
return _fetch_from_summary(key)
153+
154+
155+
def _fetch_from_summary(key: str) -> Optional[PriceData]:
156+
data = _http_get_json(SUMMARY_URL + key)
157+
if not data:
158+
return None
159+
rows = data.get("data", [])
160+
if not rows:
161+
return None
162+
row = rows[0]
163+
price = _parse_double(str(row[1]).replace(",", ""))
164+
change = 0.0
165+
try:
166+
change = _parse_double(str(row[3]).replace(",", ""))
167+
except (IndexError, ValueError):
168+
pass
169+
return PriceData(key, price, change)

0 commit comments

Comments
 (0)