From e09d75655652827131c1a986185e39c0d5088392 Mon Sep 17 00:00:00 2001 From: freqnik Date: Thu, 29 Jan 2026 00:32:20 -0500 Subject: [PATCH 01/12] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b08f91e..3b7b9d1 100755 --- a/requirements.txt +++ b/requirements.txt @@ -86,7 +86,7 @@ safe-pysha3==1.0.4 screeninfo==0.8.1 SecretStorage==3.3.3 sentinel-sdk==0.1.1 -sentinel_protobuf==0.5.0 +sentinel_protobuf==0.5.1 six==1.16.0 sniffio==1.3.1 stripe==4.2.0 From 0b7f87b569a3d4bfee5093370a11c27fea75bb8c Mon Sep 17 00:00:00 2001 From: freqnik Date: Thu, 29 Jan 2026 00:39:04 -0500 Subject: [PATCH 02/12] Add auto filter nodes on plan --- src/kv/meile.kv | 41 +++++++++++++++++++++-------------------- src/ui/widgets.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 76a7eb4..3fc97ed 100755 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -957,29 +957,30 @@ WindowManager: rows: 3 size_hint_x: 1 MDGridLayout: - cols: 7 + cols: 6 height: 25 adaptive_height: True padding: [15, 25, 0, 25] MDLabel: - text: "Plan" + text: " Plan" bold: True size_hint_x: 2.9 + halign: "center" MDLabel: - text: "Nodes" + text: " Nodes" bold: True size_hint_x: 1.1 MDLabel: - text: "Countries" + text: " Countries" bold: True size_hint_x: 1.5 MDLabel: - text: "Cost" + text: " Cost" bold: True size_hint_x: 1.4 @@ -1003,10 +1004,10 @@ WindowManager: padding: [15, 25, 0, 25] MDLabel: height: sp(35) - font_size: sp(13) + font_size: sp(15) theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.WHITE) - text: "In order to use plans that you subscribe to, click on the subscribed plan to expand it. This will select the plan. Click 'filter nodes' to find the online nodes on the plan. They will be available in the countires on the left bar. Navigate to your desired node, click on it, and then press connect." + text: "In order to use plans that you subscribe to, click on the subscribed plan to expand it. This will select the plan and automatically filter the nodes on the left bar which are on the plan (i.e., connectible). Navigate to your desired node, click on it, and then press connect." #MDGridLayout: # cols: 1 @@ -1179,7 +1180,7 @@ WindowManager: : rows: 3 - cols: 6 + cols: 5 height: 90 MDLabel @@ -1204,13 +1205,13 @@ WindowManager: text: "[b]Coin[/b]" markup: True - TooltipMDIconButton: - icon: "table-filter" - tooltip_text: "Filter Nodes" - size_hint_x: .25 - theme_text_color: "Custom" - text_color: app.theme_cls.primary_color - on_release: root.filter_nodes() + #TooltipMDIconButton: + # icon: "table-filter" + # tooltip_text: "Filter Nodes" + # size_hint_x: .25 + # theme_text_color: "Custom" + # text_color: app.theme_cls.primary_color + # on_release: root.filter_nodes() MDLabel text: root.uuid @@ -1220,27 +1221,27 @@ WindowManager: MDLabel text: root.id markup: True - font_size: sp(10) + font_size: sp(13) MDLabel text: root.expires markup: True - font_size: sp(11) + font_size: sp(13) size_hint_x: 1.3 MDLabel text: root.deposit markup: True - font_size: sp(11) + font_size: sp(13) MDLabel text: root.coin markup: True - font_size: sp(11) + font_size: sp(13) MDLabel MDLabel MDLabel MDLabel MDLabel - MDLabel + : rows: 2 diff --git a/src/ui/widgets.py b/src/ui/widgets.py index 3e5d7ee..0d32dfd 100755 --- a/src/ui/widgets.py +++ b/src/ui/widgets.py @@ -1921,6 +1921,7 @@ class PlanDetails(MDGridLayout): deposit = StringProperty() coin = StringProperty() + ''' def filter_nodes(self): mw = Meile.app.root.get_screen(WindowNames.MAIN_WINDOW) @@ -1933,7 +1934,7 @@ def filter_nodes(self): mw.NodeTree.search(key=NodeKeys.NodesInfoKeys[1], value=plan_nodes_data, perfect_match=True, is_list=True) mw.refresh_country_recycler() - + ''' class PlanAccordion(ButtonBehavior, MDGridLayout): node = ObjectProperty() # Main node info @@ -2018,14 +2019,26 @@ def on_release(self): self.remove_widget(self.children[0]) self.close_panel() self.dispatch("on_close") - + + @delayable def on_open(self, *args): """Called when a panel is opened.""" self.mw.PlanID = self.content.id + + t = Thread(target=lambda: self.filter_nodes()) + t.start() + + while t.is_alive(): + print(".", end="") + yield 0.5 + + self.mw.refresh_country_recycler() + def on_close(self, *args): """Called when a panel is closed.""" self.mw.PlanID = None + self.mw.restore_results() def close_panel(self) -> None: """Method closes the panel.""" @@ -2079,6 +2092,19 @@ def _add_content(self, *args): if self.content: self.content.y = dp(72) self.add_widget(self.content) + + def filter_nodes(self): + + try: + Request = HTTPRequests.MakeRequest() + http = Request.hadapter() + req = http.get(HTTParams.PLAN_API + HTTParams.API_PLANS_NODES % self.content.uuid, auth=HTTPBasicAuth(scrtsxx.PLANUSERNAME, scrtsxx.PLANPASSWORD)) + + plan_nodes_data = req.json() if req.status_code == 200 else None + + self.mw.NodeTree.search(key=NodeKeys.NodesInfoKeys[1], value=plan_nodes_data, perfect_match=True, is_list=True) + except: + pass class NodeCarousel(MDBoxLayout): From f3760d74d0f7acfdca7b68a5dd7d7bd9f64c36b5 Mon Sep 17 00:00:00 2001 From: freqnik Date: Tue, 10 Mar 2026 18:50:58 -0400 Subject: [PATCH 03/12] Add v2ray helper. Enable v2ray connection sharing on mobile. Make ring sessions default. Update wallet. --- src/cli/wallet.py | 23 +++++------ src/conf/meile_config.py | 4 +- src/helpers/v2ray.py | 78 ++++++++++++++++++++++++++++++++++++ src/kv/meile.kv | 27 +++++++++++++ src/typedef/konstants.py | 7 ++-- src/ui/interfaces.py | 5 ++- src/ui/screens.py | 59 ++++++++++++++++++++------- src/utils/coinimg/meile.png | Bin 0 -> 2842 bytes src/utils/qr.py | 11 ++++- 9 files changed, 180 insertions(+), 34 deletions(-) create mode 100644 src/helpers/v2ray.py create mode 100644 src/utils/coinimg/meile.png diff --git a/src/cli/wallet.py b/src/cli/wallet.py index bacec74..900787e 100644 --- a/src/cli/wallet.py +++ b/src/cli/wallet.py @@ -482,22 +482,20 @@ def subscribe(self, KEYNAME, NODE, DEPOSIT, GB, hourly): amount_required = float(DEPOSIT.replace(DENOM, "")) * IBCTokens.SATOSHI if DENOM == "udvpn": - tax = round(float(amount_required * ConfParams.SUBFEE),2) if round(float(amount_required * ConfParams.SUBFEE),2) >= 5 * IBCTokens.SATOSHI else 5 * IBCTokens.SATOSHI + tax = round(float(amount_required * ConfParams.SUBFEE),2) if round(float(amount_required * ConfParams.SUBFEE),2) >= ConfParams.SUBFEEMULT * IBCTokens.SATOSHI else ConfParams.SUBFEEMULT * IBCTokens.SATOSHI else: tax = round(float(amount_required * ConfParams.SUBFEE),2) try: + token_ibc = {v: k for k, v in IBCTokens.IBCUNITTOKEN.items()} + ubalance = balance.get(token_ibc[DENOM][1:], 0) * IBCTokens.SATOSHI + if amount_required + tax >= (ubalance + IBCTokens.SATOSHI): + self.returncode = (False, f"Balance is too low, required: {round((amount_required + tax) / IBCTokens.SATOSHI, 6)}{token_ibc[DENOM][1:]}") + return ret = self.send_2plan_wallet(KEYNAME, 31337, DENOM, tax, tax=True, bal=balance) print(ret[0]) except: pass - token_ibc = {v: k for k, v in IBCTokens.IBCUNITTOKEN.items()} - ubalance = balance.get(token_ibc[DENOM][1:], 0) * IBCTokens.SATOSHI - - if ubalance < amount_required: - self.returncode = (False, f"Balance is too low, required: {round(amount_required / IBCTokens.SATOSHI, 4)}{token_ibc[DENOM][1:]}") - return - try: result = sdk.nodes.QueryNode(address=NODE) except (mospy.exceptions.clients.TransactionTimeout, @@ -1017,16 +1015,17 @@ def connect(self, conndesc.write("Bringing up V2Ray socks tunnel...\n") conndesc.flush() decode = json.loads(decode) - + print(decode) + proxy_protocol = ["vless", "vmess"] - transport_protocol = ["gun","grpc","http","mkcp","quic","tcp","websocket"] + transport_protocol = ["domainsocket","gun","grpc","http","mkcp","quic","tcp","websocket"] #transport_security = ["none", "tls"] vmess_address = resolve_address(response['result']['addrs'][0]) vmess_port = int(decode['metadata'][0]['port']) pp = proxy_protocol[decode['metadata'][0]['proxy_protocol']-1] - #tp = transport_protocol[decode['metadata'][0]['transport_protocol']-1] - tp = transport_protocol[1] + tp = transport_protocol[decode['metadata'][0]['transport_protocol']-1] + #tp = transport_protocol[1] #ts = transport_security[decode['metadata'][0]['transport_security']-1] sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/src/conf/meile_config.py b/src/conf/meile_config.py index 1fb3f3f..4afb3cf 100755 --- a/src/conf/meile_config.py +++ b/src/conf/meile_config.py @@ -95,7 +95,9 @@ def read_configuration(self, confpath): if not self.CONFIG.has_option('network', 'dns'): self.CONFIG.set('network', 'dns', '1.1.1.1') if not self.CONFIG.has_option('network', 'ringsessions'): - self.CONFIG.set('network', 'ringsessions', '0') + self.CONFIG.set('network', 'ringsessions', '1') + if self.CONFIG.has_option('network', 'ringsessions'): + self.CONFIG.set('network', 'ringsessions', '1') FILE = open(self.CONFFILE, 'w') self.CONFIG.write(FILE) diff --git a/src/helpers/v2ray.py b/src/helpers/v2ray.py new file mode 100644 index 0000000..8aec5b9 --- /dev/null +++ b/src/helpers/v2ray.py @@ -0,0 +1,78 @@ +import json +import base64 +import sys + +def generate_v2ray_uri(config_path): + with open(config_path, 'r') as f: + config = json.load(f) + + # Find the vmess outbound + outbound = None + for o in config.get('outbounds', []): + if o.get('protocol') == 'vmess' or o.get('protocol') == 'vless': + outbound = o + break + + if not outbound: + print("No VMess outbound found in config") + sys.exit(1) + + vnext = outbound['settings']['vnext'][0] + user = vnext['users'][0] + stream = outbound.get('streamSettings', {}) + + network = stream.get('network', 'tcp') + security = stream.get('security', 'none') + + vmess = { + "v": "2", + "ps": "My VPN", # nickname, change as you like + "add": vnext['address'], + "port": str(vnext['port']), + "id": user['id'], + "aid": str(user.get('alterId', 0)), + "scy": user.get('security', 'auto'), + "net": network, + "type": "none", + "host": "", + "path": "", + "tls": "tls" if security == "tls" else "" + } + + # WebSocket settings + if network == 'ws': + ws = stream.get('wsSettings', {}) + vmess['path'] = ws.get('path', '') + vmess['host'] = ws.get('headers', {}).get('Host', '') + + # HTTP/2 settings + elif network == 'h2': + h2 = stream.get('httpSettings', {}) + vmess['path'] = h2.get('path', '') + hosts = h2.get('host', []) + vmess['host'] = hosts[0] if hosts else '' + + # gRPC settings + elif network == 'grpc': + grpc = stream.get('grpcSettings', {}) + vmess['path'] = grpc.get('serviceName', '') + vmess['type'] = 'gun' + + # TCP with HTTP obfs + elif network == 'tcp': + tcp = stream.get('tcpSettings', {}) + header = tcp.get('header', {}) + if header.get('type') == 'http': + vmess['type'] = 'http' + + # TLS SNI + tls_settings = stream.get('tlsSettings', {}) + if not vmess['host'] and tls_settings.get('serverName'): + vmess['host'] = tls_settings['serverName'] + + json_str = json.dumps(vmess, separators=(',', ':')) + b64 = base64.b64encode(json_str.encode('utf-8')).decode('utf-8') + uri = f"{o.get('protocol')}://{b64}" + + #print("\nāœ… VMess URI:\n") + return uri diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 3fc97ed..824da6b 100755 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -2855,3 +2855,30 @@ WindowManager: theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) font_size: "16sp" + +: + orientation: 'vertical' + spacing: "20dp" + padding: "20dp" + size_hint_y: None + height: "300sp" + + Image: + id: qr_img + size_hint: None, None + size: "192dp", "192dp" + pos_hint: {'center_x': 0.5} + + MDTextField: + id: uri + hint_text: "URI" + text: '' + readonly: True + selectable: True + mode: "rectangle" + MDLabel: + id: warning_comment + text: "Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this within 5 minutes of receiving this dialog." + theme_text_color: "Custom" + text_color: get_color_from_hex("f42121") + font_size: "13sp" diff --git a/src/typedef/konstants.py b/src/typedef/konstants.py index 205b845..84641f2 100755 --- a/src/typedef/konstants.py +++ b/src/typedef/konstants.py @@ -29,7 +29,8 @@ class ConfParams(): DEFAULT_SUBS = [5 * i for i in range(1, 6)] BTCPAYADJ = 1.10 XMRPAYADJ = 3.14 - SUBFEE = 0.042069 + SUBFEE = 0.1 + SUBFEEMULT = 10 class HTTParams(): # Note http://128.199.90.172:26657 is testnet ONLY! @@ -602,8 +603,8 @@ class IBCTokens(): #mu_coins = ["tsent", "udvpn", "uscrt", "uosmo", "uatom", "udec"] class TextStrings(): dash = "-" - VERSION = "2.2.2" - BUILD = "1722988800718" + VERSION = "v2.5.2" + BUILD = "17729291383" RootTag = "SENTINEL" PassedHealthCheck = "Passed Sentinel Health Check" FailedHealthCheck = "Failed Sentinel Health Check" diff --git a/src/ui/interfaces.py b/src/ui/interfaces.py index 68b8ea7..592afaf 100755 --- a/src/ui/interfaces.py +++ b/src/ui/interfaces.py @@ -122,4 +122,7 @@ class QRDialogContent(MDBoxLayout): pass class QRDialogZanoContent(MDBoxLayout): - pass \ No newline at end of file + pass + +class QRDialogV2RayContent(MDBoxLayout): + pass diff --git a/src/ui/screens.py b/src/ui/screens.py index 2b5a174..ca1afcb 100644 --- a/src/ui/screens.py +++ b/src/ui/screens.py @@ -1,5 +1,5 @@ from geography.continents import OurWorld -from ui.interfaces import LatencyContent, TooltipMDIconButton, ConnectionDialog, ProtectedLabel, IPAddressTextField, ConnectedNode, QuotaPct,BandwidthBar,BandwidthLabel, MapCenterButton, UploadLabel, DownloadLabel +from ui.interfaces import LatencyContent, TooltipMDIconButton, ConnectionDialog, ProtectedLabel, IPAddressTextField, ConnectedNode, QuotaPct,BandwidthBar,BandwidthLabel, MapCenterButton, UploadLabel, DownloadLabel,QRDialogV2RayContent from typedef.win import WindowNames from cli.sentinel import NodeTreeData from typedef.konstants import NodeKeys, TextStrings, MeileColors, HTTParams, IBCTokens, ConfParams @@ -19,6 +19,7 @@ from helpers.helpers import format_byte_size from helpers.bandwidth import compute_consumed_data, compute_consumed_hours, init_GetConsumedWhileConnected, GetConsumedWhileConnected, GetTotalDataWhileConnected from helpers.aes import SecureSeed +from helpers.v2ray import generate_v2ray_uri from kivy.properties import BooleanProperty, StringProperty, ColorProperty,ObjectProperty, NumericProperty from kivy.uix.screenmanager import Screen, SlideTransition @@ -503,20 +504,47 @@ def connect(): #print("REmove loading Widget") # Here change the Connection button to a "Disconnect" button then display dialogAdd commentMore actions self.set_protected_icon(True, Moniker) - self.dialog = MDDialog( - title="Connected!", - md_bg_color=get_color_from_hex(MeileColors.BLACK), - buttons=[ - MDFlatButton( - text="OK", - theme_text_color="Custom", - text_color=get_color_from_hex(MeileColors.MEILE), - on_release=partial(self.call_ip_get, - True, - Moniker - ) - ),]) - self.dialog.open() + if "V2Ray" in [proto, protocol]: + uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")) + connected_content = QRDialogV2RayContent() + connected_content.ids.uri.text = uri + QRcode = QRCode() + connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "meile") + + self.dialog = MDDialog( + title="Connected!", + type="custom", + content_cls=connected_content, + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDRaisedButton( + text="OK", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.BLACK), + on_release=partial(self.call_ip_get, + True, + Moniker + ) + ), + ] + ) + self.dialog.open() + + else: + self.dialog = MDDialog( + title="Connected!", + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDFlatButton( + text="OK", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.MEILE), + on_release=partial(self.call_ip_get, + True, + Moniker + ) + ),]) + self.dialog.open() else: self.remove_loading_widget2() @@ -561,6 +589,7 @@ def connect(): return elif self.SubCaller: print("Calling for a session subscription") + proto = None connect() else: diff --git a/src/utils/coinimg/meile.png b/src/utils/coinimg/meile.png new file mode 100644 index 0000000000000000000000000000000000000000..cab020798bae3a6631ad15e0f00a76a7590acb77 GIT binary patch literal 2842 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 literal 0 HcmV?d00001 diff --git a/src/utils/qr.py b/src/utils/qr.py index bf0d54c..4a7035d 100755 --- a/src/utils/qr.py +++ b/src/utils/qr.py @@ -71,10 +71,17 @@ def generate_qr_code(self, ADDRESS, coin): background.paste(QRimg, (0,0)) - if not is_ecryptfs_mounted() or coin == "dvpn": + if ADDRESS.startswith(("vmess", "vless")): + background.save(path.join(self.IMGDIR, ADDRESS[:5] + ".png")) + return path.join(self.IMGDIR, ADDRESS[0:5] + ".png") + elif not is_ecryptfs_mounted() or coin == "dvpn": background.save(path.join(self.IMGDIR, ADDRESS + ".png")) return path.join(self.IMGDIR, ADDRESS + ".png") else: hashed_address = hashlib.sha256(ADDRESS.encode()).hexdigest() background.save(path.join(self.IMGDIR, hashed_address + ".png")) - return path.join(self.IMGDIR, hashed_address + ".png") \ No newline at end of file + return path.join(self.IMGDIR, hashed_address + ".png") + + background.save(path.join(self.IMGDIR, ADDRESS + ".png")) + return path.join(self.IMGDIR, ADDRESS + ".png") + \ No newline at end of file From 124362666b32dca8f9a1843b4ab4dab9cc4cccbc Mon Sep 17 00:00:00 2001 From: freqnik Date: Fri, 13 Mar 2026 21:46:41 -0400 Subject: [PATCH 04/12] Bump kivymd==1.2.0.Add plan infographic on plan screen.Use scrollview for plan accordion Add rounded protocol text field on map Add stopwatch for timed connection Add qr-connection sharing icon in main bar. Create pulsating location marker when connected. Set ring sessions default. --- requirements.txt | 2 +- src/imgs/location_marker.png | Bin 0 -> 2045 bytes src/imgs/plans_basic.png | Bin 0 -> 70147 bytes src/imgs/plans_premium.png | Bin 0 -> 64534 bytes src/kv/meile.kv | 181 +++++++++++++++++++++++++++-------- src/typedef/konstants.py | 25 ++--- src/ui/interfaces.py | 9 +- src/ui/screens.py | 136 +++++++++++++++++++++++++- src/ui/widgets.py | 93 ++++++++++++++---- 9 files changed, 367 insertions(+), 79 deletions(-) create mode 100644 src/imgs/location_marker.png create mode 100644 src/imgs/plans_basic.png create mode 100644 src/imgs/plans_premium.png diff --git a/requirements.txt b/requirements.txt index 3b7b9d1..454c648 100755 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ keyrings.cryptfile==1.3.9 Kivy==2.2.1 Kivy-Garden==0.1.5 kivy-garden.mapview==1.0.6 -kivymd==1.1.1 +kivymd==1.2.0 kivyoav==0.42 macholib==1.16.3 mapview==1.0.6 diff --git a/src/imgs/location_marker.png b/src/imgs/location_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..8f655ffc150e4cba68c3ffedd44f76c091e654b3 GIT binary patch literal 2045 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 T9%=o27kun(U2N*C0#g44B$aMz literal 0 HcmV?d00001 diff --git a/src/imgs/plans_premium.png b/src/imgs/plans_premium.png new file mode 100644 index 0000000000000000000000000000000000000000..ac64e3b54b47f367a4b5ba3a792f1b21124a8d52 GIT binary patch literal 64534 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

>s_?gjUR<~_5+ryuQvrVp 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 literal 0 HcmV?d00001 diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 824da6b..12a46d5 100755 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -143,6 +143,13 @@ WindowManager: text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.switch_to_plan_window() + ToolTipMDIconButton: + tooltip_text: "Connection Sharing" + icon: "qrcode" + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.MEILE) + on_release: root.qrcode_connection_sharing() + ToolTipMDIconButton: tooltip_text: "Settings" icon: "cog-outline" @@ -171,10 +178,10 @@ WindowManager: id: protected - font_size: sp(26) - pos_hint: {"x" : .4, "center_y": .975} + #font_size: sp(60) + pos_hint: {"x" : .375, "center_y": .975} opacity: .75 - font_style: "H6" + font_style: "H4" text: "UNPROTECTED" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) @@ -196,8 +203,10 @@ WindowManager: text_color_focus: '#fcb711' text_color_normal: '#fcb711' #hint_text: 'IP Address:' - #fill_color_normal: get_color_from_hex("#121212") - #fill_color_focus: get_color_from_hex("#000000") + fill_color_normal: (0,0,0,0) + fill_color_focus: (0,0,0,0) + line_color_normal: '#fcb711' + line_color_focus: '#fcb711' #fill_color_normal: 0,0,0,0 #fill_color_focus: 0,0,0,0 @@ -212,11 +221,38 @@ WindowManager: opacity: .5 icon_left: "laptop" text_color_focus: '#fcb711' - text_color_normal: '#fcb711' + text_color_normal: '#fcb711' + fill_color_normal: 0, 0, 0, 0 + fill_color_focus: 0, 0, 0, 0 + line_color_normal: '#fcb711' + line_color_focus: '#fcb711' #hint_text_color_normal: '#fcb711' #hint_text_color_focus: '#fcb711' #fill_color_normal: get_color_from_hex("#121212") #fill_color_focus: get_color_from_hex("#000000") + + + id: protocol_label + mode: "round" + size_hint_x: .15 + size_hint_y: .05 + pos_hint: {"x" : 0.80, "center_y": .1} + readonly: True + opacity: .5 + icon_left: "security-network" + text: "" + hint_text: 'Protocol' + font_size: sp(12) + foreground_color: 0.988, 0.718, 0.067, 1 + background_color: 0, 0, 0, 0 + background_normal: '' + background_active: '' + text_color_focus: '#fcb711' + text_color_normal: '#fcb711' + fill_color_normal: 0, 0, 0, 0 + fill_color_focus: 0, 0, 0, 0 + line_color_normal: '#fcb711' + line_color_focus: '#fcb711' text: "Bandwidth" @@ -272,6 +308,29 @@ WindowManager: text_color: get_color_from_hex(MeileColors.UPLOAD) font_name: root.get_font() + + + font_size: "100sp" + pos_hint: {"x" : .84, "center_y": .04} + opacity: .75 + #font_style: "H6" + text: "00:00:00" + markup: True + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.MEILE) + font_name: root.get_font() + + + font_size: "100sp" + pos_hint: {"x" : .84, "center_y": .04} + opacity: .75 + #font_style: "H6" + text: "00:00:00" + markup: True + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.MEILE) + font_name: root.get_font() + id: quota_pct text: "0.00%" @@ -954,8 +1013,8 @@ WindowManager: size_hint_x: .99 pos_hint: {"center_x": 0.5} MDGridLayout: - rows: 3 - size_hint_x: 1 + rows: 5 + size_hint_x: .9 MDGridLayout: cols: 6 height: 25 @@ -998,41 +1057,81 @@ WindowManager: on_release: root.set_previous_screen() MDGridLayout: - cols: 1 + cols: 1 + rows: 4 height: sp(25) adaptive_height: True padding: [15, 25, 0, 25] - MDLabel: - height: sp(35) - font_size: sp(15) - theme_text_color: "Custom" - text_color: get_color_from_hex(MeileColors.WHITE) - text: "In order to use plans that you subscribe to, click on the subscribed plan to expand it. This will select the plan and automatically filter the nodes on the left bar which are on the plan (i.e., connectible). Navigate to your desired node, click on it, and then press connect." - - #MDGridLayout: - # cols: 1 - # height: sp(25) - # adaptive_height: True - # padding: [15, 25, 0, 25] - # MDLabel: - # height: sp(35) - # font_size: sp(14) - # theme_text_color: "Custom" - # text_color: get_color_from_hex(MeileColors.WHITE) - # text: "" + MDLabel: + height: sp(35) + font_size: sp(15) + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.WHITE) + text: "In order to use plans that you subscribe to, click on the subscribed plan to expand it. This will select the plan and automatically filter the nodes on the left bar which are on the plan (i.e., connectible). Navigate to your desired node, click on it, and then press connect." + MDLabel: + MDLabel: + + MDGridLayout: + cols: 1 + height: sp(25) + adaptive_height: True + padding: [15, 25, 0, 25] + MDLabel: + height: sp(35) + font_size: sp(14) + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.WHITE) + text: "" + MDGridLayout: + cols: 2 + height: sp(25) + adaptive_height: True + padding: [15, 25, 0, 25] + AsyncImage: + source: "../imgs/plans_basic.png" + size_hint: 1, None + size: dp(215), dp(396) + pos_hint: {"center_x": 0.5} + allow_stretch: True + keep_ratio: True + padding: [15, 10, 15, 10] + AsyncImage: + source: "../imgs/plans_premium.png" + size_hint: 1, None + size: dp(225), dp(396) + pos_hint: {"center_x": 0.5} + allow_stretch: True + keep_ratio: True + padding: [15, 10, 15, 10] - RecycleView: + #RecycleView: + # do_scroll_y: True + # size_hint_x: 1 + # size_hint_y: None + # height: sp(200) + # MDBoxLayout: + # size_hint_y: None + # size_hint_x: 1 + # adaptive_height: True + # orientation: "vertical" + # padding: [10, 10, 10, 50] + # spacing: 10 + # id: rv + ScrollView: do_scroll_y: True + do_scroll_x: False size_hint_x: 1 + size_hint_y: None + height: sp(400) + bar_width: dp(10) MDBoxLayout: + id: rv size_hint_y: None size_hint_x: 1 - adaptive_height: True - + height: self.minimum_height orientation: "vertical" padding: [10, 10, 10, 50] - spacing: 10 - id: rv + spacing: 10 : size_hint_x: .95 @@ -1085,6 +1184,8 @@ WindowManager: : rows: 2 cols: 6 + size_hint_y: None + height: self.minimum_height AsyncImage: size_hint_x: .1 width: 100 @@ -1181,8 +1282,10 @@ WindowManager: : rows: 3 cols: 5 - height: 90 - + height: 90 # different on oses + size_hint_y: None + #height: self.minimum_height + MDLabel text: "[b]UUID[/b]" markup: True @@ -1308,12 +1411,10 @@ WindowManager: : rows: 2 - #md_bg_color: get_color_from_hex("#1C1D1B") md_bg_color: get_color_from_hex(MeileColors.DIALOG_BG_COLOR2) radius: [5, 5, 5, 5] - - height: 50 - adaptive_height: True + size_hint_y: None + height: self.minimum_height : @@ -2878,7 +2979,7 @@ WindowManager: mode: "rectangle" MDLabel: id: warning_comment - text: "Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this within 5 minutes of receiving this dialog." + text: 'Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this before you disconnect in Meile. https://dvpn.my/v2ray' theme_text_color: "Custom" - text_color: get_color_from_hex("f42121") + text_color: get_color_from_hex(MeileColors.MEILE) font_size: "13sp" diff --git a/src/typedef/konstants.py b/src/typedef/konstants.py index 84641f2..ad4e5f6 100755 --- a/src/typedef/konstants.py +++ b/src/typedef/konstants.py @@ -603,8 +603,8 @@ class IBCTokens(): #mu_coins = ["tsent", "udvpn", "uscrt", "uosmo", "uatom", "udec"] class TextStrings(): dash = "-" - VERSION = "v2.5.2" - BUILD = "17729291383" + VERSION = "v2.5.3" + BUILD = "17734519923" RootTag = "SENTINEL" PassedHealthCheck = "Passed Sentinel Health Check" FailedHealthCheck = "Failed Sentinel Health Check" @@ -621,16 +621,17 @@ class MeileColors(): ROW_HOVER = "#39363c" DOWNLOAD = "#2fe548" UPLOAD = "#2fa1e5" - FONT_FACE = "../fonts/mplus-2c-bold.ttf" - FONT_FACE_ARIAL = "../fonts/arial-unicode-ms.ttf" - QR_FONT_FACE = "../fonts/Roboto-BoldItalic.ttf" - MAP_MARKER = "../imgs/location_pin.png" - LOGO = "../imgs/logo.png" - LOGO_HD = "../imgs/logo_hd.png" - LOGO_TEXT = "../imgs/logo_text.png" - SUBSCRIBE_BUTTON = "../imgs/SubscribeButton.png" - GETINFO_BUTTON = "../imgs/GetInfoButton.png" - SPINNER = "../imgs/spinner.png" + FONT_FACE = "fonts/mplus-2c-bold.ttf" + FONT_FACE_ARIAL = "fonts/arial-unicode-ms.ttf" + QR_FONT_FACE = "fonts/Roboto-BoldItalic.ttf" + MAP_MARKER = "imgs/location_pin.png" + LOC_MARKER = "imgs/location_marker.png" + LOGO = "imgs/logo.png" + LOGO_HD = "imgs/logo_hd.png" + LOGO_TEXT = "imgs/logo_text.png" + SUBSCRIBE_BUTTON = "imgs/SubscribeButton.png" + GETINFO_BUTTON = "imgs/GetInfoButton.png" + SPINNER = "imgs/spinner.png" HEALTH_ICON = "shield-plus" SICK_ICON = "emoticon-sick" ARCGIS_MAP = "https://server.arcgisonline.com/arcgis/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}.png" diff --git a/src/ui/interfaces.py b/src/ui/interfaces.py index 592afaf..9787a07 100755 --- a/src/ui/interfaces.py +++ b/src/ui/interfaces.py @@ -26,7 +26,7 @@ class ProtectedLabel(MDLabel): def get_font(self): Config = MeileGuiConfig() - return Config.resource_path(MeileColors.QR_FONT_FACE) + return Config.resource_path(MeileColors.FONT_FACE) class DownloadLabel(MDLabel): def get_font(self): @@ -37,6 +37,10 @@ class UploadLabel(MDLabel): def get_font(self): Config = MeileGuiConfig() return Config.resource_path(MeileColors.FONT_FACE) +class TimeLabel(MDLabel): + def get_font(self): + Config = MeileGuiConfig() + return Config.resource_path(MeileColors.FONT_FACE) class MapCenterButton(MDIconButton, MDTooltip): pass @@ -47,6 +51,9 @@ class ToolTipMDIconButton(MDIconButton, MDTooltip): class IPAddressTextField(MDTextField): pass +class ProtocolTextField(MDTextField): + pass + class ConnectedNode(MDTextField): pass diff --git a/src/ui/screens.py b/src/ui/screens.py index ca1afcb..22c33c4 100644 --- a/src/ui/screens.py +++ b/src/ui/screens.py @@ -1,5 +1,5 @@ from geography.continents import OurWorld -from ui.interfaces import LatencyContent, TooltipMDIconButton, ConnectionDialog, ProtectedLabel, IPAddressTextField, ConnectedNode, QuotaPct,BandwidthBar,BandwidthLabel, MapCenterButton, UploadLabel, DownloadLabel,QRDialogV2RayContent +from ui.interfaces import LatencyContent, TooltipMDIconButton, ConnectionDialog, ProtectedLabel, IPAddressTextField, ProtocolTextField, ConnectedNode, QuotaPct,BandwidthBar,BandwidthLabel, MapCenterButton, UploadLabel, DownloadLabel,QRDialogV2RayContent, TimeLabel from typedef.win import WindowNames from cli.sentinel import NodeTreeData from typedef.konstants import NodeKeys, TextStrings, MeileColors, HTTParams, IBCTokens, ConfParams @@ -42,7 +42,7 @@ from kivy.uix.floatlayout import FloatLayout from kivymd.uix.anchorlayout import MDAnchorLayout from kivymd.uix.label.label import MDLabel - +from kivy.animation import Animation from kivy.app import App import requests @@ -51,7 +51,7 @@ import copy from copy import deepcopy import re -from time import sleep +from time import sleep, time from functools import partial from shutil import rmtree from os import path,geteuid, chdir, remove @@ -362,7 +362,9 @@ class MainWindow(Screen): dnscrypt = False PlanConnect = False HourlyFirstRun = True - + location_marker = None + is_running = False + start_time = 0 def __init__(self, node_tree, **kwargs): @@ -504,6 +506,7 @@ def connect(): #print("REmove loading Widget") # Here change the Connection button to a "Disconnect" button then display dialogAdd commentMore actions self.set_protected_icon(True, Moniker) + self.toggle_time_widget() if "V2Ray" in [proto, protocol]: uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")) connected_content = QRDialogV2RayContent() @@ -545,6 +548,8 @@ def connect(): ) ),]) self.dialog.open() + + else: self.remove_loading_widget2() @@ -610,6 +615,7 @@ def connect(): else: self.disconnect_from_node() self.HourlyFirstRun = True + self.reset_stopwatch() try: self.clock.cancel() except: @@ -621,6 +627,71 @@ def connect(): except: print("No Clock Bytes... Yet") self.clockBytes = None + + def toggle_time_widget(self): + if not self.is_running: + self.start_stopwatch() + else: + self.stop_stopwatch() + + def start_stopwatch(self): + self.is_running = True + if self.start_time == 0: + self.start_time = time() + Clock.schedule_interval(self.update_time, 1.0) + + + def stop_stopwatch(self): + self.is_running = False + Clock.unschedule(self.update_time) + + def update_time(self, dt): + if self.is_running: + self.elapsed_time = time() - self.start_time + self.time_widget.text = self.format_time(self.elapsed_time) + return True + + def format_time(self, seconds): + td = timedelta(seconds=int(seconds)) + hours = td.seconds // 3600 + minutes = (td.seconds % 3600) // 60 + secs = td.seconds % 60 + return f"{hours:02d}:{minutes:02d}:{secs:02d}" + + def reset_stopwatch(self): + self.is_running = False + self.start_time = 0 + self.elapsed_time = 0 + self.time_widget.text = '00:00:00' + Clock.unschedule(self.update_time) + + def qrcode_connection_sharing(self): + if path.isfile(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")): + uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")) + else: + uri = "NULL. Start a V2Ray connection in Meile first." + + connected_content = QRDialogV2RayContent() + connected_content.ids.uri.text = uri + QRcode = QRCode() + connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "meile") + + self.dialog = MDDialog( + title="Connection Sharing", + type="custom", + content_cls=connected_content, + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDRaisedButton( + text="OK", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.BLACK), + on_release=self.closeDialog + ), + ] + ) + self.dialog.open() + def setTotalBytesClock(self): self.clockBytes = Clock.create_trigger(self.GetUpDownBytes,10) @@ -857,6 +928,8 @@ def build_meile_map(self): self.map_widget_1 = IPAddressTextField() self.map_widget_2 = ConnectedNode() self.map_widget_3 = ProtectedLabel() + self.map_widget_4 = ProtocolTextField() + self.time_widget = TimeLabel() self.upload_widget = UploadLabel() self.download_widget = DownloadLabel() recenter = MapCenterButton() @@ -876,12 +949,15 @@ def build_meile_map(self): layout.add_widget(self.map_widget_1) layout.add_widget(self.map_widget_2) layout.add_widget(self.map_widget_3) + layout.add_widget(self.map_widget_4) layout.add_widget(bw_label) layout.add_widget(self.quota) layout.add_widget(self.quota_pct) layout.add_widget(recenter) layout.add_widget(self.upload_widget) layout.add_widget(self.download_widget) + layout.add_widget(self.time_widget) + self.quota.value = 0 self.quota_pct.text = "0%" @@ -1160,6 +1236,7 @@ def get_ip_address(self, dt, startup: bool = False): self.LatLong.append(loc[1]) if not startup: self.zoom_country_map() + self.add_location_marker() return True except Exception as e: print(str(e)) @@ -1503,16 +1580,59 @@ def call_ip_get(self,result, moniker, *kwargs): self.remove_loading_widget(None) self.close_sub_window() + def add_location_marker(self): + self.remove_location_marker() + Config = MeileGuiConfig() + + with open(path.join(ConfParams.KEYRINGDIR, 'ip-api.json'), 'r') as f: + data = f.read() + + ifJSON = json.loads(data) + if not ifJSON: + return False + + self.location_marker = MapMarkerPopup(lat=self.LatLong[0], lon=self.LatLong[1], source=Config.resource_path(MeileColors.LOC_MARKER)) + self.location_marker.add_widget(MDMapCountryButton(text='%s, %s' %(ifJSON['city'], ifJSON['country']), + theme_text_color="Custom", + md_bg_color=get_color_from_hex(MeileColors.BLACK), + text_color=(1,1,1,1), + )) + + anim = ( + Animation(opacity=0.4, duration=0.8, t='in_out_sine') + + Animation(opacity=1.0, duration=0.8, t='in_out_sine') + ) + anim.repeat = True + anim.start(self.location_marker) + + + self.Markers.append(self.location_marker) + self.MeileMap.add_marker(self.location_marker) + + def remove_location_marker(self): + if self.location_marker: + self.MeileMap.remove_marker(self.location_marker) + self.Markers.remove(self.Markers[-1]) + def set_protected_icon(self, setbool, moniker): try: if setbool: self.map_widget_2.text = moniker self.map_widget_3.text = "PROTECTED" + anim = ( + Animation(opacity=0.2, duration=0.8, t='in_out_sine') + + Animation(opacity=1.0, duration=0.8, t='in_out_sine') + ) + anim.repeat = True + anim.start(self.map_widget_3) + self.map_widget_4.text = self.NodeCarouselData['protocol'] self.ids.connect_button.source = self.return_connect_button("d") else: self.map_widget_2.text = moniker self.map_widget_3.text = "UNPROTECTED" + Animation.cancel_all(self.map_widget_3) + self.map_widget_4.text = "" self.ids.connect_button.source = self.return_connect_button("c") except Exception as e: print(str(e)) @@ -1545,6 +1665,14 @@ def return_connect_button(self, text): button_path = "../imgs/DisconnectButton.png" return MeileConfig.resource_path(button_path) + def closeDialog(self, inst): + try: + self.dialog.dismiss() + self.dialog = None + except: + print("Dialog is NONE") + return + class WalletScreen(Screen): text = StringProperty() ADDRESS = None diff --git a/src/ui/widgets.py b/src/ui/widgets.py index 0d32dfd..b780ebc 100755 --- a/src/ui/widgets.py +++ b/src/ui/widgets.py @@ -1995,22 +1995,9 @@ def __init__(self, **kwargs): def finish_init(self, dt): self.add_widget(self.node) - + ''' def on_release(self): - '''TODO: - in first logic statement populate a MainScreen dictionary - with current node address and ID. - THis will be used when the user clicks on the subscription - which expands it's contents, the MainScreen dictionary - will be used to connect to subscription when the user - clicks "Connect" - Second logic statement (else) should reset the MainScreen - dictionary to prior state. - - Use: - content.node_address - content.sub_id - ''' + if len(self.children) == 1: self.add_widget(self.content) self.open_panel() @@ -2019,7 +2006,15 @@ def on_release(self): self.remove_widget(self.children[0]) self.close_panel() self.dispatch("on_close") - + ''' + def on_release(self): + if len(self.children) == 1: + self.open_panel() + self.dispatch("on_open") + else: + self.close_panel() + self.dispatch("on_close") + @delayable def on_open(self, *args): """Called when a panel is opened.""" @@ -2039,7 +2034,7 @@ def on_close(self, *args): """Called when a panel is closed.""" self.mw.PlanID = None self.mw.restore_results() - + ''' def close_panel(self) -> None: """Method closes the panel.""" @@ -2056,7 +2051,8 @@ def close_panel(self) -> None: ) anim.bind(on_complete=self._disable_anim) anim.start(self) - + + def open_panel(self, *args) -> None: """Method opens a panel.""" @@ -2074,17 +2070,72 @@ def open_panel(self, *args) -> None: # anim.bind(on_complete=self._add_content) anim.bind(on_complete=self._disable_anim) anim.start(self) - + ''' + def close_panel(self) -> None: + if self._anim_playing: + return + + self._anim_playing = True + self._state = "close" + + # Store the target height BEFORE removing + target_h = self.node.height + + anim = Animation( + height=target_h, + d=self.closing_time, + t=self.closing_transition, + ) + anim.bind(on_complete=self._on_close_complete) + anim.start(self) + + def _on_close_complete(self, *args): + self._anim_playing = False + # Remove content AFTER animation finishes + if self.content in self.children: + self.remove_widget(self.content) + + def open_panel(self, *args) -> None: + if self._anim_playing: + return + + self._anim_playing = True + self._state = "open" + + # Add content first so we can measure it + if self.content not in self.children: + self.add_widget(self.content) + + # Current node height + content height + target_h = self.node.height + self.content.height + + # Temporarily keep current height so animation works + current_h = self.node.height + self.height = current_h + + anim = Animation( + height=target_h, + d=self.opening_time, + t=self.opening_transition, + ) + anim.bind(on_complete=self._disable_anim) + anim.start(self) + + def get_state(self) -> str: """Returns the state of panel. Can be `close` or `open` .""" return self._state - + ''' def add_widget(self, widget, index=0, canvas=None): if isinstance(widget, NodeDetails): self.height = widget.height return super().add_widget(widget) - + ''' + def add_widget(self, widget, index=0, canvas=None): + # Don't override height here — let the animation handle it + return super().add_widget(widget, index=index, canvas=canvas) + def _disable_anim(self, *args): self._anim_playing = False From c65562f268a6410cba94bbbf4153bf71508cac7c Mon Sep 17 00:00:00 2001 From: freqnik Date: Mon, 30 Mar 2026 02:43:25 -0400 Subject: [PATCH 05/12] Fix MainWindow layout to prevent floating widgets and improve resize UX --- src/kv/meile.kv | 193 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 133 insertions(+), 60 deletions(-) diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 12a46d5..13f558f 100755 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -44,19 +44,33 @@ WindowManager: MDBoxLayout: orientation: 'vertical' md_bg_color: get_color_from_hex("#121212") - MDFloatLayout: - size_hint_y: .1 - + + # ── Top Bar ── + MDBoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: dp(56) + padding: [dp(8), 0, dp(8), 0] + + # Left: logo icon Image: source: root.get_logo() size_hint: None, None - height: sp(50) - pos_hint: {'x' : .0001, 'center_y' : .5} + size: dp(50), dp(50) + pos_hint: {'center_y': .5} + + # Left: logo text Image: source: root.get_logo_text() + size_hint_x: None + width: dp(150) size_hint_y: .6 - size_hint_x: .6 - pos_hint: {'x' : -.15, 'center_y' : .5} + pos_hint: {'center_y': .5} + + # Spacer pushes right-side icons to the end + Widget: + size_hint_x: 1 + ToolTipMDIconButton: id: doh opacity: 0 @@ -65,7 +79,8 @@ WindowManager: theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.INDICATOR) on_release: root.menu_open() - pos_hint: {'x' : .90, 'center_y' : .5} + pos_hint: {'center_y': .5} + ToolTipMDIconButton: id: settings_menu tooltip_text: "Menu" @@ -73,90 +88,104 @@ WindowManager: theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.menu_open() - pos_hint: {'x' : .95, 'center_y' : .5} - + pos_hint: {'center_y': .5} + + # ── Main Content ── MDBoxLayout: id: country_map orientation: "horizontal" + + # Left sidebar MDBoxLayout: orientation: "vertical" - size_hint: (.25, 1) - MDFloatLayout: - size_hint: None,.175 + size_hint_x: .25 + + # ── Connect button + Search area ── + MDBoxLayout: + orientation: 'vertical' + size_hint_y: None + height: dp(110) + padding: [dp(8), dp(16), dp(8), dp(4)] + spacing: dp(4) + + # Connect button — own row, centered, large TooltipMDFlatButton: - #tooltip_text: "Connect to Node" - #md_bg_color: get_color_from_hex("#121212") - pos_hint: {'x' : .4, 'y': .6} + size_hint: None, None + size: dp(70), dp(40) + pos_hint: {'center_x': .2} on_release: root.connect_routine() - opacity: 1 Image: id: connect_button - size_hint: 3,3 + size_hint: 2.2, 2.2 + pos: self.parent.pos + size: self.parent.size source: root.return_connect_button("c") - - - MDTextField: - opacity: 1 - id: search_box - hint_text: "key: , value:" - icon_right: "magnify" - pos_hint: {'x' : .1, 'y': 0} - size_hint_x: 1.9 - size_hint_y: .6 - font_size: sp(12) - on_text_validate: root.on_enter_search() - - ToolTipMDIconButton: - tooltip_text: "Clear" - pos_hint: {'x' : 2.1, 'y': .145} - on_release: root.restore_results() - icon: "restore" - theme_text_color: "Custom" - text_color: get_color_from_hex(MeileColors.MEILE) + + # Search row: text field + clear button + MDBoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: dp(48) + spacing: dp(4) + + MDTextField: + id: search_box + hint_text: "key: , value:" + icon_right: "magnify" + size_hint_x: 1 + font_size: sp(12) + on_text_validate: root.on_enter_search() + + ToolTipMDIconButton: + tooltip_text: "Clear" + on_release: root.restore_results() + icon: "restore" + size_hint_x: None + width: dp(48) + theme_text_color: "Custom" + text_color: get_color_from_hex(MeileColors.MEILE) + + # ── Node list ── NodeRV: - md_bg_color: get_color_from_hex("#121212") + md_bg_color: get_color_from_hex("#121212") id: rv - size_hint: (1,1) - MDBoxLayout: - size_hint: (1,None) - height: 50 - spacing: 6 - + size_hint_y: 1 # fills remaining vertical space + + # ── Bottom toolbar ── + MDBoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: dp(48) + spacing: dp(6) + ToolTipMDIconButton: tooltip_text: "Wallet" icon: "wallet-outline" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.build_wallet_interface() - - #ToolTipMDIconButton: - # tooltip_text: "Subscriptions" - # icon: "book-open-outline" - # theme_text_color: "Custom" - # text_color: get_color_from_hex(MeileColors.MEILE) - # on_release: root.switch_to_sub_window() - + ToolTipMDIconButton: tooltip_text: "Plans" icon: "ballot-outline" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.switch_to_plan_window() - - ToolTipMDIconButton: + + ToolTipMDIconButton: tooltip_text: "Connection Sharing" icon: "qrcode" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.qrcode_connection_sharing() - + ToolTipMDIconButton: tooltip_text: "Settings" icon: "cog-outline" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) on_release: root.build_settings_screen_interface() - + ToolTipMDIconButton: tooltip_text: "Help" icon: "help-circle-outline" @@ -307,8 +336,7 @@ WindowManager: theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.UPLOAD) font_name: root.get_font() - - + font_size: "100sp" pos_hint: {"x" : .84, "center_y": .04} @@ -2979,7 +3007,52 @@ WindowManager: mode: "rectangle" MDLabel: id: warning_comment - text: 'Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this before you disconnect in Meile. https://dvpn.my/v2ray' + text: "" theme_text_color: "Custom" text_color: get_color_from_hex(MeileColors.MEILE) font_size: "13sp" + + + orientation: "vertical" + spacing: "1sp" + size_hint_y: None + height: "130sp" + price_text: "" + naddress: " " + moniker: " " + deposit: " " + MDFloatLayout: + MDLabel: + id: sub_type + text: "Select Protocol" + theme_text_color: "Custom" + font_style: "Subtitle2" + font_size: "20sp" + text_color: get_color_from_hex(MeileColors.MEILE) + pos_hint: {"x" : 0, "top" : 1.35} + MDLabel: + id: bandwidth_text + text: "Wireguard" + theme_text_color: "Custom" + font_style: "Subtitle2" + font_size: "14sp" + text_color: get_color_from_hex("#ffffff") + pos_hint: {"x" : 0, "top" : 1} + Check: + group: 'proto' + on_active: root.select_share_type(self, self.active, "wg") + pos_hint: {"x": .5, "y" : .30} + MDLabel: + pos_hint: {"x" : 0, "top" : .9} + MDLabel: + id: hourly_text + text: "V2Ray" + theme_text_color: "Custom" + font_style: "Subtitle2" + font_size: "14sp" + text_color: get_color_from_hex("#ffffff") + pos_hint: {"x" : 0, "top" : .7} + Check: + group: 'proto' + on_active: root.select_share_type(self, self.active, "v2") + pos_hint: {"x": .5, "y" : .025} \ No newline at end of file From 6f5a287df096dc7e3fc43f5969831826392bc64a Mon Sep 17 00:00:00 2001 From: freqnik Date: Tue, 31 Mar 2026 22:36:00 -0400 Subject: [PATCH 06/12] Create fetch_credentials(), write_wireguard_config() in wallet module. Add QR-code sharing selection dialog for both protocol types. Add update_checker to provide a dialog on main window if user's app is not the latest github version. --- src/cli/wallet.py | 516 ++++++++++++++++++++++++++-------- src/helpers/update_checker.py | 98 +++++++ src/typedef/konstants.py | 6 +- src/ui/screens.py | 120 ++++---- src/ui/update_dialog.py | 82 ++++++ src/ui/widgets.py | 64 ++++- 6 files changed, 722 insertions(+), 164 deletions(-) create mode 100644 src/helpers/update_checker.py create mode 100644 src/ui/update_dialog.py diff --git a/src/cli/wallet.py b/src/cli/wallet.py index 900787e..45c85d1 100644 --- a/src/cli/wallet.py +++ b/src/cli/wallet.py @@ -650,8 +650,234 @@ def get_random_node_addresses(self, node_tree: Tree, count: int = 8) -> list: addresses.append(node.data['Address']) print(f"Nodes for ring sessions: {addresses}") - return addresses + return addresses + + def fetch_credentials(self, address, session_id, type, conndesc, renew_sdk: bool = False): + if renew_sdk: + CONFIG = MeileConfig.read_configuration(MeileConfig.CONFFILE) + PASSWORD = CONFIG['wallet'].get('password', '') + KEYNAME = CONFIG['wallet'].get('keyname', '') + self.GRPC = CONFIG['network'].get('grpc', HTTParams.GRPC) + grpcaddr, grpcport = self.GRPC.split(":") + kr = self.__keyring(PASSWORD) + private_key = kr.get_password("meile-gui", KEYNAME) + try: + self.sdk = SDKInstance(grpcaddr, int(grpcport), secret=private_key, ssl=True) + except ConnectionError: + message = "gRPC unresponsive. Try again later or switch gRPCs." + self.connected = {"v2ray_pid" : None, + "result" : False, + "status" : message, + "session_id" : None} + return + except grpc._channel._InactiveRpcError as e: + status_code = e.code() + + if status_code == StatusCode.NOT_FOUND: + message = "Wallet not found on blockchain. Please verify you have sent coins to your wallet to activate it. Then try your subscription again" + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : message, + "session_id" : None} + print(self.connected) + return + else: + message = "gRPC Error!" + self.connected = {"v2ray_pid" : None, + "result" : False, + "status" : message, + "session_id" : None} + return + + + node = self.sdk.nodes.QueryNode(address) + + for _ in range(0, 10): + pub_key_bytes = self.sdk._account.public_key.key + pub_key_b64 = base64.b64encode(pub_key_bytes).decode("utf-8") + pub_key = f"secp256k1:{pub_key_b64}" + + if type == "WireGuard": + wgkey = WgKey() + key = wgkey.pubkey + data = {'public_key': key} + data_bytes = json.dumps(data).encode('utf-8') + else: # NodeType.V2RAY + self.uid_16 = uuid.uuid4() + data = {'uuid': list(self.uid_16.bytes)} + data_bytes = json.dumps(data).encode('utf-8') + + sk = ecdsa.SigningKey.from_string( + self.sdk._account.private_key, + curve=ecdsa.SECP256k1, + hashfunc=hashlib.sha256 + ) + + bige_session = int(session_id).to_bytes(8, byteorder="big") + msg = bige_session + data_bytes + signature = sk.sign(msg) + + payload = { + "data": base64.b64encode(data_bytes).decode("utf-8"), + "id": int(session_id), + "pub_key": pub_key, + "signature": base64.b64encode(signature).decode("utf-8"), + } + + print(f"\nPayload: {json.dumps(payload, indent=4)}") + conndesc.write("Fetching credentials from node...\n") + conndesc.flush() + try: + response = requests.post( + f"https://{node.remote_addrs[0]}/", + json=payload, + headers={ + "Content-Type": "application/json; charset=utf-8" + }, + verify=False, + timeout=17 + ) + except ( + ReadTimeout, ConnectionError, ConnectionRefusedError + ) as e: + print(str(e)) + status = ("Timeout while trying to fetch credentials " + "from node...Exiting\n") + conndesc.write(status) + conndesc.flush() + conndesc.close() + self.connected = { + "v2ray_pid": None, + "result": False, + "status": status + } + print(self.connected) + return None, None, None + except NewConnectionError as e: + print(str(e)) + status = ("Timeout while trying to fetch credentials " + "from node...Exiting\n") + conndesc.write(status) + conndesc.flush() + conndesc.close() + self.connected = { + "v2ray_pid": None, + "result": False, + "status": status + } + print(self.connected) + return None, None, None + + print(response, response.text) + + if response.ok is True: + break + + sleep(random.uniform(0.5, 1)) + # Continue iteration only for code == 4 (invalid signature) + if response.json()["error"]["code"] != 2: + break + + if response.ok is False: + self.connected = { + "v2ray_pid": None, + "result": False, + "status": response.text + } + print(self.connected) + return None, None, None + + response = response.json() + if response.get("success", True) is True: + response_dict = response["result"] + decode = base64.b64decode( + response_dict['data'] + ).decode('utf-8') + print(f"\nDecode: {decode}") + print(f"\nlength: {len(decode)}") + + wgkey_out = wgkey if type == "WireGuard" else None + return response, decode, wgkey_out + + return None, None, None + + def write_wireguard_config(self, response, decode, wgkey, conndesc, iface): + if len(decode) < 100: + self.connected = { + "v2ray_pid": None, + "result": False, + "status": f"Incorrect result size: {len(decode)}" + } + print(self.connected) + return None + + decode = json.loads(decode) + conndesc.write("Bringing up dVPN tunnel...\n") + conndesc.flush() + + ipv4_address = decode['addrs'][0] + if len(decode['addrs']) > 1: + ipv6_address = decode['addrs'][1] + else: + ipv6_address = "" + + # Here should check if client is using ipv6. + # This will give ipv4 + host = response['result']['addrs'][0] + port = decode['metadata'][0]['port'] + peer_endpoint = f"{host}:{port}" + + print("ipv4_address", ipv4_address) + print("ipv6_address", ipv6_address) + print("host", host) + print("port", port) + print("peer_endpoint", peer_endpoint) + + public_key = decode['metadata'][0]['public_key'] + print("public_key", public_key) + + config = configparser.ConfigParser() + config.optionxform = str + + # [from golang] listenPort, err := netutil.GetFreeUDPPort() + sock = socket.socket() + sock.bind(('', 0)) + listen_port = sock.getsockname()[1] + sock.close() + + address = ( + ",".join([ipv4_address, ipv6_address]) + if ipv6_address + else ipv4_address + ) + + config.add_section("Interface") + config.set("Interface", "Address", address) + config.set("Interface", "ListenPort", f"{listen_port}") + config.set("Interface", "PrivateKey", wgkey.privkey) + config.set( + "Interface", "DNS", + ",".join(["127.0.0.1", "1.0.0.1", "1.1.1.1"]) + ) + + config.add_section("Peer") + config.set("Peer", "PublicKey", public_key) + config.set("Peer", "Endpoint", peer_endpoint) + config.set("Peer", "AllowedIPs", ",".join(["0.0.0.0/0", "::/0"])) + config.set("Peer", "PersistentKeepalive", "25") + + + config_file = path.join(ConfParams.KEYRINGDIR, f"{iface}.conf") + + if path.isfile(config_file) is True: + remove(config_file) + + with open(config_file, "w", encoding="utf-8") as f: + config.write(f) + + return config_file + def connect(self, ID, address, @@ -687,24 +913,33 @@ def connect(self, private_key = kr.get_password("meile-gui", KEYNAME) try: - sdk = SDKInstance(grpcaddr, int(grpcport), secret=private_key, ssl=True) + self.sdk = SDKInstance(grpcaddr, int(grpcport), secret=private_key, ssl=True) except ConnectionError: message = "gRPC unresponsive. Try again later or switch gRPCs." - self.connected = {"v2ray_pid" : None, "result" : False, "status" : message} + self.connected = {"v2ray_pid" : None, + "result" : False, + "status" : message, + "session_id" : None} return except grpc._channel._InactiveRpcError as e: status_code = e.code() if status_code == StatusCode.NOT_FOUND: message = "Wallet not found on blockchain. Please verify you have sent coins to your wallet to activate it. Then try your subscription again" - self.connected = {"v2ray_pid" : None, "result": False, "status" : message} + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : message, + "session_id" : None} print(self.connected) return else: message = "gRPC Error!" - self.connected = {"v2ray_pid" : None, "result" : False, "status" : message} + self.connected = {"v2ray_pid" : None, + "result" : False, + "status" : message, + "session_id" : None} return - + regex_denom = r'^([\d\.]+)(.*)$' regres = re.match(regex_denom, deposit) @@ -745,7 +980,7 @@ def connect(self, if plan: try: if not RINGSESSIONS: - tx = sdk.subscriptions.StartSession(subscription_id=int(ID), address=address, tx_params=tx_params) + tx = self.sdk.subscriptions.StartSession(subscription_id=int(ID), address=address, tx_params=tx_params) else: addresses = self.get_random_node_addresses(NodeTree, count=8) rand_index = random.randint(0, len(addresses)) @@ -756,7 +991,7 @@ def connect(self, conndesc.flush() if addr == address: - tx = sdk.subscriptions.StartSession( + tx = self.sdk.subscriptions.StartSession( subscription_id=int(ID), address=addr, next_sequence = True if k > 1 else False, @@ -764,7 +999,7 @@ def connect(self, ) print(tx) else: - tx_temp = sdk.subscriptions.StartSession( + tx_temp = self.sdk.subscriptions.StartSession( subscription_id=int(ID), address=addr, next_sequence = True if k > 1 else False, @@ -772,7 +1007,7 @@ def connect(self, ) print(tx_temp) - print(f"Sequence after tx: {sdk.subscriptions._account.next_sequence}") + print(f"Sequence after tx: {self.sdk.subscriptions._account.next_sequence}") sleep(0.1) k += 1 @@ -784,7 +1019,10 @@ def connect(self, conndesc.write("GRPC Error... Exiting") conndesc.flush() conndesc.close() - self.connected = {"v2ray_pid" : None, "result": False, "status" : details} + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : details, + "session_id" : None} print(self.connected) return else: @@ -795,7 +1033,7 @@ def connect(self, try: - tx = sdk.nodes.SubscribeToNode(node_address=address, + tx = self.sdk.nodes.SubscribeToNode(node_address=address, price=sprice, gigabytes=0 if hourly else int(units), hours=int(units) if hourly else 0, @@ -808,18 +1046,24 @@ def connect(self, conndesc.write("GRPC Error... Exiting") conndesc.flush() conndesc.close() - self.connected = {"v2ray_pid" : None, "result": False, "status" : details} + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : details, + "session_id" : None} print(self.connected) return # Will need to handle log responses with friendly UI response in case of session create error if tx.get("log", None) is not None: - self.connected = {"v2ray_pid" : None, "result": False, "status" : tx["log"]} + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : tx["log"], + "session_id" : None} print(self.connected) return try: - tx_response = sdk.subscriptions.wait_for_tx(tx["hash"], timeout=25) + tx_response = self.sdk.subscriptions.wait_for_tx(tx["hash"], timeout=25) except (mospy.exceptions.clients.TransactionTimeout, mospy.exceptions.clients.NodeException, mospy.exceptions.clients.NodeTimeoutException) as e: @@ -827,16 +1071,30 @@ def connect(self, conndesc.write("GRPC Error... Exiting") conndesc.flush() conndesc.close() - self.connected = {"v2ray_pid" : None, "result": False, "status" : "GRPC Error"} + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : "GRPC Error", + "session_id" : None} return session_id = search_attribute(tx_response, "sentinel.subscription.v3.EventCreateSession" if plan else "sentinel.node.v3.EventCreateSession", "session_id") + + ''' + from_event = { + "subscription_id": search_attribute(tx_response, "sentinel.subscription.v3.EventCreateSession", "subscription_id"), + "address": search_attribute(tx_response, "sentinel.subscription.v3.EventCreateSession", "acc_address"), + "node_address": search_attribute(tx_response, "sentinel.subscription.v3.EventCreateSession", "node_address"), + } + ''' - sleep(0.3) # Wait a few seconds.... + # Sanity Check. Not needed + #assert from_event["subscription_id"] == ID and from_event["address"] == sdk._account.address and from_event["node_address"] == address + + sleep(0.2) # Wait a few seconds.... # The sleep is required because the session_id could not be fetched from the node / rpc - + ''' node = sdk.nodes.QueryNode(address) # Get Client PubKey @@ -980,102 +1238,140 @@ def connect(self, with open(config_file, "w", encoding="utf-8") as f: config.write(f) - - if pltfrm == Arch.LINUX: - child = pexpect.spawn(f"pkexec sh -c 'ip link delete {iface}; wg-quick up {config_file}'") - child.expect(pexpect.EOF) - sleep(7) - elif pltfrm == Arch.OSX: - connectBASH = [sentinel_connect_bash] - proc2 = subprocess.Popen(connectBASH) - proc2.wait(timeout=30) - pid2 = proc2.pid - proc_out, proc_err = proc2.communicate() - elif pltfrm == Arch.WINDOWS: - wgup = [gsudo, MeileConfig.WIREGUARD_BIN, "/installtunnelservice", config_file] - wg_process = subprocess.Popen(wgup) - sleep(15) - - if psutil.net_if_addrs().get(iface): - self.connected = {"v2ray_pid" : None, "result": True, "status" : iface} - conndesc.write("Checking network connection...\n") - conndesc.flush() - sleep(1) - self.get_ip_address() - sleep(1) - conndesc.close() - return - else: - self.connected = {"v2ray_pid" : None, "result": False, "status" : "Error bringing up wireguard interface"} - return + ''' + response, decode, wgkey = self.fetch_credentials( + address, session_id, type, conndesc + ) + + if response is None or decode is None: + status = "Node response is null. Terminating..." + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : status, + "session_id" : session_id} + print(self.connected) + return + + if type == "WireGuard": + iface = "wg99" + config_file = self.write_wireguard_config( + response, decode, wgkey, conndesc, iface + ) + + if config_file is None: + status = "Error writing WireGuard config. Terminating..." + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : status, + "session_id" : session_id} + print(self.connected) + return + + - else: # v2ray - # os x - #chdir(MeileConfig.BASEBINDIR) - conndesc.write("Bringing up V2Ray socks tunnel...\n") - conndesc.flush() - decode = json.loads(decode) - print(decode) + if pltfrm == Arch.LINUX: + child = pexpect.spawn(f"pkexec sh -c 'ip link delete {iface}; wg-quick up {config_file}'") + child.expect(pexpect.EOF) + sleep(5) + elif pltfrm == Arch.OSX: + connectBASH = [sentinel_connect_bash] + proc2 = subprocess.Popen(connectBASH) + proc2.wait(timeout=30) + pid2 = proc2.pid + proc_out, proc_err = proc2.communicate() + #subprocess.run(["sudo", "launchctl", "load", str(LAUNCHDAEMON_PATH)], check=True) + sleep(3) + elif pltfrm == Arch.WINDOWS: + wgup = [gsudo, MeileConfig.WIREGUARD_BIN, "/installtunnelservice", config_file] + wg_process = subprocess.Popen(wgup) + sleep(15) - proxy_protocol = ["vless", "vmess"] - transport_protocol = ["domainsocket","gun","grpc","http","mkcp","quic","tcp","websocket"] - #transport_security = ["none", "tls"] + if psutil.net_if_addrs().get(iface) or psutil.net_if_addrs().get("utun3"): + self.connected = {"v2ray_pid" : None, + "result": True, + "status" : iface, + "session_id" : session_id} + conndesc.write("Checking network connection...\n") + conndesc.flush() + sleep(1) + self.get_ip_address() + sleep(1) + conndesc.close() + return + else: + self.connected = {"v2ray_pid" : None, + "result": False, + "status" : "Error bringing up wireguard interface", + "session_id" : session_id} + return + + else: # v2ray + # os x + chdir(MeileConfig.BASEBINDIR) + conndesc.write("Bringing up V2Ray socks tunnel...\n") + conndesc.flush() + decode = json.loads(decode) + print(decode) + + proxy_protocol = ["vless", "vmess"] + transport_protocol = ["domainsocket","gun","grpc","http","mkcp","quic","tcp","websocket"] + #transport_security = ["none", "tls"] + + vmess_address = resolve_address(response['result']['addrs'][0]) + vmess_port = int(decode['metadata'][0]['port']) + pp = proxy_protocol[decode['metadata'][0]['proxy_protocol']-1] + tp = transport_protocol[decode['metadata'][0]['transport_protocol']-1] + #tp = transport_protocol[1] + #ts = transport_security[decode['metadata'][0]['transport_security']-1] + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('', 0)) + api_port = sock.getsockname()[1] + sock.close() + + print("api_port", api_port) + print("vmess_port", vmess_port) + print("vmess_address", vmess_address) + print("vmess_uid", f"{self.uid_16}") + print("vmess_transport", tp) + print("proxy_protocol", pp) + + if self.FRAGMENT: + v2ray_config = V2RayFragmentConfiguration( + api_port=api_port, + vmess_port=vmess_port, + vmess_address=vmess_address, + vmess_uid=f"{self.uid_16}", + vmess_transport=tp, + proxy_port=1080, + proxy_protocol=pp + ) + else: + v2ray_config = V2RayConfiguration( + api_port=api_port, + vmess_port=vmess_port, + vmess_address=vmess_address, + vmess_uid=f"{self.uid_16}", + vmess_transport=tp, + proxy_port=1080, + proxy_protocol=pp + ) + # ConfParams.KEYRINGDIR (.meile-gui) + config_file = path.join(ConfParams.KEYRINGDIR, "v2ray_config.json") + if path.isfile(config_file) is True: + remove(config_file) + with open(config_file, "w", encoding="utf-8") as f: + f.write(json.dumps(v2ray_config.get(), indent=4)) - vmess_address = resolve_address(response['result']['addrs'][0]) - vmess_port = int(decode['metadata'][0]['port']) - pp = proxy_protocol[decode['metadata'][0]['proxy_protocol']-1] - tp = transport_protocol[decode['metadata'][0]['transport_protocol']-1] - #tp = transport_protocol[1] - #ts = transport_security[decode['metadata'][0]['transport_security']-1] - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(('', 0)) - api_port = sock.getsockname()[1] - sock.close() - - print("api_port", api_port) - print("vmess_port", vmess_port) - print("vmess_address", vmess_address) - print("vmess_uid", f"{uid_16}") - print("vmess_transport", tp) - print("proxy_protocol", pp) + proxy_ip_file = path.join(ConfParams.KEYRINGDIR, "v2ray.proxy") + if path.isfile(proxy_ip_file) is True: + remove(proxy_ip_file) + with open(proxy_ip_file, "w", encoding="utf-8") as f: + f.write(vmess_address) - if self.FRAGMENT: - v2ray_config = V2RayFragmentConfiguration( - api_port=api_port, - vmess_port=vmess_port, - vmess_address=vmess_address, - vmess_uid=f"{uid_16}", - vmess_transport=tp, - proxy_port=1080, - proxy_protocol=pp - ) - else: - v2ray_config = V2RayConfiguration( - api_port=api_port, - vmess_port=vmess_port, - vmess_address=vmess_address, - vmess_uid=f"{uid_16}", - vmess_transport=tp, - proxy_port=1080, - proxy_protocol=pp - ) - # ConfParams.KEYRINGDIR (.meile-gui) - config_file = path.join(ConfParams.KEYRINGDIR, "v2ray_config.json") - if path.isfile(config_file) is True: - remove(config_file) - with open(config_file, "w", encoding="utf-8") as f: - f.write(json.dumps(v2ray_config.get(), indent=4)) - - proxy_ip_file = path.join(ConfParams.KEYRINGDIR, "v2ray.proxy") - if path.isfile(proxy_ip_file) is True: - remove(proxy_ip_file) - with open(proxy_ip_file, "w", encoding="utf-8") as f: - f.write(vmess_address) - - # v2ray_tun2routes_connect_bash - # >> hardcoded = proxy port >> 1080 - # >> hardcoded = v2ray file >> /home/${USER}/.meile-gui/v2ray_config.json + # v2ray_tun2routes_connect_bash + # >> hardcoded = proxy port >> 1080 + # >> hardcoded = v2ray file >> /home/${USER}/.meile-gui/v2ray_config.json tuniface = False v2ray_handler = V2RayHandler(f"{v2ray_tun2routes_connect_bash} up") diff --git a/src/helpers/update_checker.py b/src/helpers/update_checker.py new file mode 100644 index 0000000..1bf4bee --- /dev/null +++ b/src/helpers/update_checker.py @@ -0,0 +1,98 @@ +import requests +import re +from packaging import version +from typedef.konstants import TextStrings + + + +class UpdateChecker: + + def __init__(self): + self.latest_version = None + self.current_version = TextStrings.VERSION + self.release_notes = [] + self.update_available = False + + def check_for_update(self): + try: + headers = { + "Accept": "application/vnd.github+json", + "User-Agent": "Meile-dVPN-UpdateChecker" + } + response = requests.get(TextStrings.GITHUB_API_URL, headers=headers, timeout=10) + response.raise_for_status() + data = response.json() + + self.latest_version = data.get("tag_name", "").strip() + body = data.get("body", "") + self.release_notes = self._parse_whats_new(body, max_items=3) + + current_clean = self._strip_v(self.current_version) + latest_clean = self._strip_v(self.latest_version) + + if version.parse(latest_clean) > version.parse(current_clean): + self.update_available = True + return { + "current_version": self.current_version, + "latest_version": self.latest_version, + "release_notes": self.release_notes, + "download_url": TextStrings.DOWNLOAD_URL, + } + + return None + + except requests.RequestException as e: + print(f"[UpdateChecker] Network error: {e}") + return None + except Exception as e: + print(f"[UpdateChecker] Error: {e}") + return None + + @staticmethod + def _strip_v(version_string: str) -> str: + return version_string.lstrip("vV") + + @staticmethod + def _parse_whats_new(body: str, max_items: int = 2) -> list: + lines = body.split("\n") + in_whats_new = False + items = [] + + for line in lines: + stripped = line.strip() + + if re.match(r"^(#{1,6}\s+)?\*{0,2}What'?s\s+New\*{0,2}\s*$", + stripped, re.IGNORECASE): + in_whats_new = True + continue + + if in_whats_new: + bullet_match = re.match(r"^[-*]\s+(.+)$", stripped) + if bullet_match: + items.append(bullet_match.group(1).strip()) + if len(items) >= max_items: + break + elif stripped.startswith("#") or ( + stripped.startswith("**") and stripped.endswith("**") + ): + break + + return items + +def format_update_message(update_info: dict) -> str: + latest = update_info["latest_version"] + current = update_info["current_version"] + notes = update_info["release_notes"] + url = update_info["download_url"] + + msg = f"Meile dVPN [b]{latest}[/b] is now available! (You have {current})\n\n" + + if notes: + msg += "[b]What's New:[/b]\n" + for note in notes: + msg += f" • {note}\n" + msg += " & more!\n\n" + + msg += f"Visit [color=#3CDAB7][ref=download]{url}[/ref][/color] to download the latest release." + + return msg \ No newline at end of file diff --git a/src/typedef/konstants.py b/src/typedef/konstants.py index ad4e5f6..7dd8396 100755 --- a/src/typedef/konstants.py +++ b/src/typedef/konstants.py @@ -603,9 +603,11 @@ class IBCTokens(): #mu_coins = ["tsent", "udvpn", "uscrt", "uosmo", "uatom", "udec"] class TextStrings(): dash = "-" - VERSION = "v2.5.3" - BUILD = "17734519923" + VERSION = "v2.5.1" + BUILD = "17748518213" RootTag = "SENTINEL" + GITHUB_API_URL = "https://api.github.com/repos/MathNodes/meile-gui/releases/latest" + DOWNLOAD_URL = "https://meile.app" PassedHealthCheck = "Passed Sentinel Health Check" FailedHealthCheck = "Failed Sentinel Health Check" diff --git a/src/ui/screens.py b/src/ui/screens.py index 22c33c4..a02558e 100644 --- a/src/ui/screens.py +++ b/src/ui/screens.py @@ -5,7 +5,7 @@ from typedef.konstants import NodeKeys, TextStrings, MeileColors, HTTParams, IBCTokens, ConfParams from cli.sentinel import disconnect as Disconnect import main.main as Meile -from ui.widgets import WalletInfoContent, SeedInfoContent, MDMapCountryButton, RatingContent, NodeRV, NodeRV2, NodeAccordion, NodeRow, NodeDetails, PlanAccordion, PlanRow, PlanDetails, NodeCarousel, SubTypeDialog, SubscribeContent, LoadingSpinner +from ui.widgets import WalletInfoContent, SeedInfoContent, MDMapCountryButton, RatingContent, NodeRV, NodeRV2, NodeAccordion, NodeRow, NodeDetails, PlanAccordion, PlanRow, PlanDetails, NodeCarousel, SubTypeDialog, SubscribeContent, LoadingSpinner, ShareTypeDialog from utils.qr import QRCode from cli.wallet import HandleWalletFunctions from conf.meile_config import MeileGuiConfig @@ -20,6 +20,8 @@ from helpers.bandwidth import compute_consumed_data, compute_consumed_hours, init_GetConsumedWhileConnected, GetConsumedWhileConnected, GetTotalDataWhileConnected from helpers.aes import SecureSeed from helpers.v2ray import generate_v2ray_uri +from helpers.update_checker import UpdateChecker, format_update_message +from ui.update_dialog import UpdateDialog from kivy.properties import BooleanProperty, StringProperty, ColorProperty,ObjectProperty, NumericProperty from kivy.uix.screenmanager import Screen, SlideTransition @@ -365,6 +367,7 @@ class MainWindow(Screen): location_marker = None is_running = False start_time = 0 + hwf = None def __init__(self, node_tree, **kwargs): @@ -396,7 +399,7 @@ def __init__(self, node_tree, **kwargs): width_mult=3, position="center", max_height=max_height, - background_color=get_color_from_hex(MeileColors.BLACK)) + md_bg_color=get_color_from_hex(MeileColors.BLACK)) def update_wallet(self, dt): MeileConfig = MeileGuiConfig() @@ -448,11 +451,11 @@ def connect(): pass - hwf = HandleWalletFunctions() + self.hwf = HandleWalletFunctions() thread = Thread(target=lambda: self.ping()) thread.start() if not self.SubCaller: - t = Thread(target=lambda: hwf.connect(ID, + t = Thread(target=lambda: self.hwf.connect(ID, naddress, proto, deposit, @@ -460,7 +463,7 @@ def connect(): plan=PlanConnect)) t.start() else: - t = Thread(target=lambda: hwf.connect(0, + t = Thread(target=lambda: self.hwf.connect(0, node, protocol, sub_deposit, @@ -488,10 +491,10 @@ def connect(): #conndesc.close() self.cd.ids.pb.value = 1 - self.ConnectedDict = deepcopy(hwf.connected) + self.ConnectedDict = deepcopy(self.hwf.connected) yield 0.420 try: - if hwf.connected['result']: + if self.hwf.connected['result']: print("CONNECTED!!!") self.CONNECTED = True Moniker = self.NodeCarouselData['moniker'] @@ -507,32 +510,42 @@ def connect(): # Here change the Connection button to a "Disconnect" button then display dialogAdd commentMore actions self.set_protected_icon(True, Moniker) self.toggle_time_widget() + #if "V2Ray" in [proto, protocol]: + connected_content = QRDialogV2RayContent() + QRcode = QRCode() if "V2Ray" in [proto, protocol]: uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")) - connected_content = QRDialogV2RayContent() connected_content.ids.uri.text = uri - QRcode = QRCode() - connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "meile") - - self.dialog = MDDialog( - title="Connected!", - type="custom", - content_cls=connected_content, - md_bg_color=get_color_from_hex(MeileColors.BLACK), - buttons=[ - MDRaisedButton( - text="OK", - theme_text_color="Custom", - text_color=get_color_from_hex(MeileColors.BLACK), - on_release=partial(self.call_ip_get, - True, - Moniker - ) - ), - ] - ) - self.dialog.open() - + connected_content.ids.warning_comment.text = "Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this before you disconnect in Meile. https://dvpn.my/v2ray" + connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "v2ray") + else: + WG_PATH = path.join(ConfParams.KEYRINGDIR,"wg99.conf") + with open(WG_PATH, "r") as f: + wg_config = f.read() + wg_config = wg_config.replace("127.0.0.1,", "") + connected_content.ids.uri.text = wg_config + connected_content.ids.warning_comment.text = "Scan the QR code or input the config with the official Wireguard app. You must do this before you disconnect in Meile. https://dvpn.my/wireguard" + connected_content.ids.qr_img.source = QRcode.generate_wg_qr_code(WG_PATH, self.NodeCarouselData['moniker']) + + self.dialog = MDDialog( + title="Connected!", + type="custom", + content_cls=connected_content, + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDRaisedButton( + text="OK", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.BLACK), + on_release=partial(self.call_ip_get, + True, + Moniker + ) + ), + ] + ) + self.dialog.open() + ''' else: self.dialog = MDDialog( title="Connected!", @@ -549,14 +562,14 @@ def connect(): ),]) self.dialog.open() - + ''' else: self.remove_loading_widget2() self.dialog = MDDialog( title="Something went wrong. Not connected: ", - text=hwf.connected['status'] if hwf.connected['status'] else "Connection Error", + text=self.hwf.connected['status'] if self.hwf.connected['status'] else "Connection Error", md_bg_color=get_color_from_hex(MeileColors.BLACK), buttons=[ MDFlatButton( @@ -587,6 +600,7 @@ def connect(): if self.PlanID: ID = self.PlanID naddress = self.NodeCarouselData['address'] + self.node_address = deepcopy(naddress) proto = self.NodeCarouselData['protocol'] deposit = "dvpn" PlanConnect = True @@ -666,29 +680,14 @@ def reset_stopwatch(self): Clock.unschedule(self.update_time) def qrcode_connection_sharing(self): - if path.isfile(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")): - uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR,"v2ray_config.json")) - else: - uri = "NULL. Start a V2Ray connection in Meile first." - - connected_content = QRDialogV2RayContent() - connected_content.ids.uri.text = uri - QRcode = QRCode() - connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "meile") + + share_content = ShareTypeDialog() self.dialog = MDDialog( - title="Connection Sharing", + title="Protocol to Connection Share", type="custom", - content_cls=connected_content, + content_cls=share_content, md_bg_color=get_color_from_hex(MeileColors.BLACK), - buttons=[ - MDRaisedButton( - text="OK", - theme_text_color="Custom", - text_color=get_color_from_hex(MeileColors.BLACK), - on_release=self.closeDialog - ), - ] ) self.dialog.open() @@ -851,6 +850,11 @@ def build(self, dt): thread = Thread(target=lambda: self.nonblock_get_ip_address(self.get_ip_address, True)) thread.start() + if not getattr(self, '_update_checked', False): + self._update_checked = True + Thread(target=lambda: self._check_update_background(),daemon=True).start() + + def create_new_wallet(self): hwf = HandleWalletFunctions() @@ -968,6 +972,20 @@ def build_meile_map(self): self.AddCountryNodePins(False) self.MeileMapBuilt = True + def _check_update_background(self): + checker = UpdateChecker() + update_info = checker.check_for_update() + + if update_info is not None: + Clock.schedule_once(lambda dt: self._show_update_dialog(update_info), 1.0) + + def _show_update_dialog(self, update_info): + message = format_update_message(update_info) + UpdateDialog( + message=message, + download_url=update_info["download_url"] + ).show() + def check_boundaries(self, instance, value): if self.MeileMap.zoom == 1: self.recenter_map() diff --git a/src/ui/update_dialog.py b/src/ui/update_dialog.py new file mode 100644 index 0000000..46863c6 --- /dev/null +++ b/src/ui/update_dialog.py @@ -0,0 +1,82 @@ +import webbrowser + +from typedef.konstants import TextStrings, MeileColors + +from kivy.metrics import dp, sp +from kivy.properties import StringProperty +from kivy.lang import Builder +from kivy.utils import get_color_from_hex + +from kivymd.uix.dialog import MDDialog +from kivymd.uix.button import MDFlatButton, MDRaisedButton +from kivymd.uix.boxlayout import MDBoxLayout + +Builder.load_string(""" +: + orientation: "vertical" + spacing: dp(12) + padding: [dp(16), dp(8), dp(16), dp(8)] + size_hint_y: None + height: self.minimum_height + adaptive_height: True + + MDLabel: + id: update_message_label + text: root.message_text + markup: True + theme_text_color: "Custom" + text_color: 1, 1, 1, 0.87 + font_size: sp(14) + size_hint_y: None + height: self.texture_size[1] + on_ref_press: root.open_link(args[1]) +""") + +class UpdateDialogContent(MDBoxLayout): + message_text = StringProperty("") + + def open_link(self, ref_name): + if ref_name == "download": + webbrowser.open(TextStrings.DOWNLOAD_URL) + +class UpdateDialog: + + def __init__(self, message: str, download_url: str = TextStrings.DOWNLOAD_URL): + self.message = message + self.download_url = download_url + self.dialog = None + + def show(self): + if self.dialog is None: + content = UpdateDialogContent(message_text=self.message) + + self.dialog = MDDialog( + title="[color=#3CDAB7]Update Available[/color]", + type="custom", + content_cls=content, + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDFlatButton( + text="LATER", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.MEILE), + on_release=lambda *_: self.dismiss(), + ), + MDRaisedButton( + text="DOWNLOAD", + md_bg_color=get_color_from_hex(MeileColors.MEILE), + text_color=get_color_from_hex(MeileColors.BLACK), + on_release=lambda *_: self.open_download(), + ), + ], + ) + + self.dialog.open() + + def dismiss(self): + if self.dialog: + self.dialog.dismiss() + + def open_download(self): + webbrowser.open(self.download_url) + self.dismiss() \ No newline at end of file diff --git a/src/ui/widgets.py b/src/ui/widgets.py index b780ebc..64a7674 100755 --- a/src/ui/widgets.py +++ b/src/ui/widgets.py @@ -49,11 +49,12 @@ from cli.btcpay import BTCPayDB import main.main as Meile from adapters import HTTPRequests -from ui.interfaces import TXContent, ConnectionDialog, QRDialogContent, QRDialogZanoContent +from ui.interfaces import TXContent, ConnectionDialog, QRDialogContent, QRDialogZanoContent, QRDialogV2RayContent from coin_api.get_price import GetPriceAPI from adapters.ChangeDNS import ChangeDNS from kivy.uix.recyclegridlayout import RecycleGridLayout from helpers.helpers import format_byte_size +from helpers.v2ray import generate_v2ray_uri from fiat.stripe_pay.dist import scrtsxx from utils.qr import QRCode @@ -164,6 +165,67 @@ def on_release(self): def update_size(self, *args): self.size = self.texture_size # Ensure label size matches text size + +class ShareTypeDialog(BoxLayout): + + def select_share_type(self, instance, value, proto): + if not value: + return + mw = Meile.app.root.get_screen(WindowNames.MAIN_WINDOW) + mw.closeDialog(None) + #self.closeDialog() + iface = "wg99" + WG_PATH = path.join(ConfParams.KEYRINGDIR,f"{iface}.conf") + V2_PATH = path.join(ConfParams.KEYRINGDIR, "v2ray_config.json") + connected_content = QRDialogV2RayContent() + QRcode = QRCode() + if proto == "wg": + if path.isfile(WG_PATH): + with open(WG_PATH, "r") as f: + wg_config = f.read() + wg_config = wg_config.replace("127.0.0.1,", "") + connected_content.ids.uri.text = wg_config + connected_content.ids.qr_img.source = QRcode.generate_wg_qr_code(WG_PATH, "WireGuard") + + else: + connected_content.ids.uri.text = "Start a WireGuard connection in Meile first." + connected_content.ids.qr_img.source = QRcode.generate_qr_code("NULL", "wireguard") + connected_content.ids.warning_comment.text = 'Scan the QR code or import the wireguard config strings into the official WireGuard mobile app. You MUST first disconnect from the Wireguard node in Meile before connecting on mobile. https://dvpn.my/wireguard' + + else: + if path.isfile(V2_PATH): + uri = generate_v2ray_uri(path.join(ConfParams.KEYRINGDIR, "v2ray_config.json")) + else: + uri = "Start a V2Ray connection in Meile first." + + connected_content.ids.uri.text = uri + connected_content.ids.qr_img.source = QRcode.generate_qr_code(uri, "v2ray") + connected_content.ids.warning_comment.text = 'Scan the QR code or import the URI string into the V2RayNG mobile app. You must do this before you disconnect in Meile. https://dvpn.my/v2ray' + + + self.dialog = MDDialog( + title="Connection Sharing", + type="custom", + content_cls=connected_content, + md_bg_color=get_color_from_hex(MeileColors.BLACK), + buttons=[ + MDRaisedButton( + text="OK", + theme_text_color="Custom", + text_color=get_color_from_hex(MeileColors.BLACK), + on_release=self.closeDialog + ), + ] + ) + self.dialog.open() + + def closeDialog(self, inst): + try: + self.dialog.dismiss() + self.dialog = None + except: + print("Dialog is NONE") + return class SubTypeDialog(BoxLayout): From 81488164f274fe791d8516cce7d7e7106d4a4469 Mon Sep 17 00:00:00 2001 From: freqnik Date: Wed, 1 Apr 2026 21:18:55 -0400 Subject: [PATCH 07/12] Update qr.py --- src/utils/qr.py | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/utils/qr.py b/src/utils/qr.py index 4a7035d..c754a2f 100755 --- a/src/utils/qr.py +++ b/src/utils/qr.py @@ -19,6 +19,85 @@ def __init__(self): self.BASEDIR = MeileGuiConfig.BASEDIR self.IMGDIR = MeileGuiConfig.IMGDIR self.MeileConfig = MeileGuiConfig() + + def generate_wg_qr_code(self, conf_path, label=None): + + + with open(conf_path, 'r') as f: + wg_config = f.read().strip() + + wg_config = wg_config.replace("127.0.0.1,", "") + + if not label: + label = path.basename(conf_path) + + + wg_logo_path = self.MeileConfig.resource_path( + 'utils/coinimg/wireguard.png' + ) + has_logo = path.exists(wg_logo_path) + + QRcode = qrcode.QRCode( + error_correction=qrcode.constants.ERROR_CORRECT_H + ) + QRcode.add_data(wg_config) + QRcode.make() + + QRimg = QRcode.make_image( + fill_color='Black', back_color='white' + ).convert('RGB') + + if has_logo: + logo = Image.open(wg_logo_path) + basewidth = 100 + wpercent = (basewidth / float(logo.size[0])) + hsize = int(float(logo.size[1]) * float(wpercent)) + logo = logo.resize((basewidth, hsize)) + + pos = ( + (QRimg.size[0] - logo.size[0]) // 2, + (QRimg.size[1] - logo.size[1]) // 2, + ) + QRimg.paste(logo, pos) + + + border = (0, 4, 0, 30) + QRimg = ImageOps.crop(QRimg, border) + + if len(label) <= 50: + fontSize = 16 + elif len(label) <= 75: + fontSize = 12 + else: + fontSize = 11 + + background = Image.new( + 'RGBA', + (QRimg.size[0], QRimg.size[1] + 15), + (255, 255, 255, 255), + ) + robotoFont = ImageFont.truetype( + self.MeileConfig.resource_path( + 'utils/fonts/Roboto-BoldItalic.ttf' + ), + fontSize, + ) + + draw = ImageDraw.Draw(background) + _, _, w, h = draw.textbbox((0, 0), text=str(label)) + draw.text( + ((QRimg.size[0] + 15 - w) / 2, QRimg.size[1] - 2), + label, + (0, 0, 0), + font=robotoFont, + ) + + background.paste(QRimg, (0, 0)) + + safe_name = path.splitext(path.basename(conf_path))[0] + out_path = path.join(self.IMGDIR, safe_name + '_wg.png') + background.save(out_path) + return out_path def generate_qr_code(self, ADDRESS, coin): DepositCoin = coin From bb52a39af516b83efed853e063b4afeb832c8568 Mon Sep 17 00:00:00 2001 From: freqnik Date: Wed, 1 Apr 2026 21:28:44 -0400 Subject: [PATCH 08/12] add wireguard asset --- src/utils/coinimg/wireguard.png | Bin 0 -> 3730 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/utils/coinimg/wireguard.png diff --git a/src/utils/coinimg/wireguard.png b/src/utils/coinimg/wireguard.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1b88303d552f280955c61c611e1c10ad6ce9a1 GIT binary patch literal 3730 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 Date: Wed, 1 Apr 2026 21:31:56 -0400 Subject: [PATCH 09/12] add v2ray asset --- src/utils/coinimg/v2ray.png | Bin 0 -> 5009 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/utils/coinimg/v2ray.png diff --git a/src/utils/coinimg/v2ray.png b/src/utils/coinimg/v2ray.png new file mode 100644 index 0000000000000000000000000000000000000000..21eed8535e88d058c19e2e6bc2eb2466c938dae2 GIT binary patch literal 5009 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 z z#d`*zqQqlC{8L*9t?#X6iZk^bXI$gSiIC@c4GD_WCGnaf+$fT0atBA~|IiQc4%xFT zp6X&;?1trZT(gnTf)2m1p+1&02p!HyhVX>C8dRX^ zufnr~jDLBjp9$8^wkcw_Z6>mzx30ga*LTSC^rL`4Y;g}~-E|{-|LQ;S4wRdXuDG*^ z5Pio|IwrYOG@Drzt_OUdnZ#{vxGmT*wO`nh5zJ<_+ z-q&|FNX~E~6kXKKvaepx(8nLZd#yy|ngw*e^Fm_hEb^sWyv+*F>?QNLCn2w! zIYl7$*)4=OE^j#X{d%*sGnfiT7Im}qE7xPECTLr;w4pFM$s9`#=?C^u{I|ZT-DnTu zcUm^GU{Gd%}LOSVn!=>aldcoN3FNk`TcXhrM`CpP{>r$A;}%*K@3 znr@r4@Z9*6r&W_8DLbTow1dKTUqk6>@ zku;-UeH3rcIL35+07DV`{8mC6Pl4@a_8-y}4bn7BEWz;S z`f}B@X>$N#pS_syrkRXC)eS05;A`#SI1X;*`-=1D&-1@In#<)F&rUQATHC8l&C!`( z)71)%T443>^1)HYKJhc;u+zM%L5O~43*k*GCN*Oxk55hiV^UorlbgAf@wL`y&%<#Y zY}-L=kIv3HT)pi|9{?@F@#CWu@`XwPQ7;~953Cx;Y`eo%qN#UutM}N4Qe^)27w99E zDl!0(zuQcxf$gi~>FQTtCp}-+I>U|KV%^kvZU(@`^L)`a7z%Rrwkt>^5&ZTzv6X5{Winq-p&6yogXLmB?nZ6pKY1*KMABgIB<0!2hF24=raAeGWf}$@$t3gU&t<{<1sH}= bQ>y+Cdkj_q+DhC^00000NkvXXu0mjf`gVU2 literal 0 HcmV?d00001 From 936cc23651d3574cb277fb941189ea7d7a3c793e Mon Sep 17 00:00:00 2001 From: freqnik Date: Mon, 6 Apr 2026 01:54:19 -0400 Subject: [PATCH 10/12] unify v2ray.py across all OSes --- src/cli/v2ray.py | 608 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 493 insertions(+), 115 deletions(-) diff --git a/src/cli/v2ray.py b/src/cli/v2ray.py index 582e7ad..dd5cf8f 100755 --- a/src/cli/v2ray.py +++ b/src/cli/v2ray.py @@ -1,48 +1,428 @@ -from subprocess import Popen +import subprocess +from subprocess import Popen, PIPE import multiprocessing from multiprocessing import Process from time import sleep from dataclasses import dataclass -import psutil +import sys +import os -from typedef.konstants import ConfParams from conf.meile_config import MeileGuiConfig -class V2RayHandler(): +# Platform-conditional imports +if sys.platform == 'win32': + import psutil + import netifaces + import json + from os import path + import win32gui, win32con + from typedef.konstants import ConfParams +elif sys.platform == 'darwin': + import tempfile +elif sys.platform.startswith('linux'): + import psutil + from typedef.konstants import ConfParams + +# --------------------------------------------------------------------------- +# V2RayHandler – one class per platform, selected at the bottom of this +# section via V2RayHandler = _LinuxV2RayHandler | _WindowsV2RayHandler | … +# --------------------------------------------------------------------------- + +class _LinuxV2RayHandler(): v2ray_script = None v2ray_pid = None - + def __init__(self, script, **kwargs): self.v2ray_script = script print(f"v2ray_script: {self.v2ray_script}") print(self.v2ray_script) - + def fork_v2ray(self): - v2ray_daemon_cmd = 'pkexec env PATH=%s %s' %(ConfParams.PATH, self.v2ray_script) - v2ray_srvc_proc = Popen(v2ray_daemon_cmd, shell=True,close_fds=True) - + v2ray_daemon_cmd = ( + 'pkexec env PATH=%s %s' + % (ConfParams.PATH, self.v2ray_script) + ) + v2ray_srvc_proc = Popen( + v2ray_daemon_cmd, shell=True, close_fds=True + ) + print("PID: %s" % v2ray_srvc_proc.pid) - + self.v2ray_pid = v2ray_srvc_proc.pid - def start_daemon(self): - + print("Starting v2ray service...") - + multiprocessing.get_context('fork') warp_fork = Process(target=self.fork_v2ray) warp_fork.run() sleep(1.5) return True - + + def kill_daemon(self): + v2ray_daemon_cmd = ( + 'pkexec env PATH=%s %s' + % (ConfParams.PATH, self.v2ray_script) + ) + proc2 = Popen(v2ray_daemon_cmd, shell=True) + proc2.wait(timeout=30) + proc_out, proc_err = proc2.communicate() + return proc2.returncode + +class _WindowsV2RayHandler(): + MeileConfig = MeileGuiConfig() + v2ray_script = None + v2ray_pid = None + tunproc = "tun2socks.exe" + v2rayproc = "xray.exe" + CREATE_NO_WINDOW = 0x08000000 + CREATE_NEW_CONSOLE = 0x00000010 + WINDOW_TITLE = "meile_v2ray_daemon" + + def __init__(self, script, **kwargs): + self.v2ray_script = script + print(self.v2ray_script) + + def fork_v2ray(self): + # Use "title" command so we can find the window + # reliably by its exact title + v2ray_daemon_cmd = ( + 'cmd.exe /c start "%s" cmd.exe /k gsudo.exe %s' + % (self.WINDOW_TITLE, self.v2ray_script) + ) + v2ray_srvc_proc = Popen( + v2ray_daemon_cmd, + shell=True, + stdout=PIPE, + stderr=PIPE + ) + sleep(5) + print("PID: %s" % v2ray_srvc_proc.pid) + + # Find and hide the window by our known title + hwnd = self._find_window_by_title(self.WINDOW_TITLE) + if hwnd: + print("Found window hwnd=%s, hiding..." % hwnd) + win32gui.ShowWindow(hwnd, win32con.SW_HIDE) + else: + print("WARNING: Could not find cmd window to hide") + + self.v2ray_pid = v2ray_srvc_proc.pid + + def _find_window_by_title(self, title): + """ + Enumerate all windows and find the one whose + title contains our unique identifier. + """ + result = [] + + def callback(hwnd, _): + window_title = win32gui.GetWindowText(hwnd) + if title in window_title: + result.append(hwnd) + return True + + win32gui.EnumWindows(callback, None) + + if result: + return result[0] + return None + + def start_daemon(self): + + print("Starting v2ray service...") + + routes_bat = 'routes.bat' + gateways = netifaces.gateways() + + default_gateway = gateways[netifaces.AF_INET][0][0] + + SERVER = self.read_v2ray_config() + + batfile = open(routes_bat, 'w') + + batfile.write( + 'CD "%s"\n' % self.MeileConfig.BASEBINDIR + ) + batfile.write( + 'START "" /B %s run -c %s\n' + % ( + self.v2rayproc, + path.join( + self.MeileConfig.BASEDIR, + "v2ray_config.json" + ), + ) + ) + batfile.write('timeout /t 1\n') + batfile.write( + 'START "" /B %s -device tun://tun00' + ' -proxy socks5://127.0.0.1:1080"\n' + % self.tunproc + ) + batfile.write('timeout /t 2\n') + batfile.write( + 'netsh interface ip set address "tun00"' + ' static address=10.10.10.2' + ' mask=255.255.255.0 gateway=10.10.10.1\n' + ) + batfile.write( + 'netsh interface ip set dns name="tun00"' + ' static 1.1.1.1\n' + ) + batfile.write( + 'route add %s %s metric 5\n' + % (SERVER, default_gateway) + ) + batfile.write( + 'route add 0.0.0.0 mask 0.0.0.0 10.10.10.1' + ) + batfile.flush() + batfile.close() + + self.v2ray_script = routes_bat + + self.fork_v2ray() + sleep(3) + + return True + def kill_daemon(self): - v2ray_daemon_cmd = 'pkexec env PATH=%s %s' %(ConfParams.PATH, self.v2ray_script) + + SERVER = self.read_v2ray_config() + gateways = netifaces.gateways() + default_gateway = gateways[netifaces.AF_INET][0][0] + + routes_bat = 'delroutes.bat' + + batfile = open(routes_bat, 'w') + + batfile.write( + 'route delete %s %s metric 5\n' + % (SERVER, default_gateway) + ) + batfile.write( + 'route delete 0.0.0.0 mask 0.0.0.0 10.10.10.1\n' + ) + batfile.write( + 'netsh interface set interface' + ' name="tun00" disable\n' + ) + batfile.write('timeout /t 3\n') + batfile.write('TASKKILL /F /IM tun2socks.exe\n') + batfile.write('TASKKILL /F /IM xray.exe\n') + batfile.flush() + batfile.close() + + self.v2ray_script = routes_bat + + # Use our known title so we can find/kill it + v2ray_daemon_cmd = ( + 'gsudo.exe %s' % (self.v2ray_script) + ) proc2 = Popen(v2ray_daemon_cmd, shell=True) proc2.wait(timeout=30) - proc_out,proc_err = proc2.communicate() + proc_out, proc_err = proc2.communicate() + + # Kill the hidden cmd.exe window we spawned + hwnd = self._find_window_by_title(self.WINDOW_TITLE) + if hwnd: + win32gui.PostMessage( + hwnd, win32con.WM_CLOSE, 0, 0 + ) + return proc2.returncode - + + def read_v2ray_config(self): + + with open( + path.join( + self.MeileConfig.BASEDIR, 'v2ray_config.json' + ), + 'r', + ) as V2RAYFILE: + v2ray = V2RAYFILE.read() + + JSON = json.loads(v2ray) + + return ( + JSON['outbounds'][0]['settings']['vnext'][0]['address'] + ) + +class _DarwinV2RayHandler: + v2ray_pid = 0 + + def __init__(self, script, **kwargs): + self.script_path = script + self.processes = [] + self.MeileConfig = MeileGuiConfig() + + def run_privileged_script(self, commands): + script_content = "#!/bin/bash\n" + script_content += "\n".join(commands) + + with tempfile.NamedTemporaryFile( + mode='w', suffix='.sh', delete=False + ) as f: + f.write(script_content) + temp_script_path = f.name + + os.chmod(temp_script_path, 0o755) + + applescript = f''' + tell application "System Events" + do shell script "{temp_script_path}" with administrator privileges + end tell + ''' + + try: + proc = subprocess.Popen( + ['osascript', '-e', applescript], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + stdout, stderr = proc.communicate(timeout=120) + + os.unlink(temp_script_path) + + if proc.returncode == 0: + return True + else: + print( + "Privileged script failed with" + " return code %s" % proc.returncode + ) + print(f"STDERR: {stderr}") + return False + + except subprocess.TimeoutExpired: + print("Privileged execution timed out") + try: + proc.kill() + except: + pass + os.unlink(temp_script_path) + return False + except Exception as e: + print(f"Error running privileged script: {e}") + os.unlink(temp_script_path) + return False + + def run_cmd(self, cmd, background=False): + if background: + process = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return process + else: + return subprocess.run(cmd, shell=True, timeout=30) + + def start_daemon(self): + print("Starting v2ray service...") + + privileged_commands = [ + "launchctl bootstrap system" + " /Library/LaunchDaemons/app.meile.xray.plist" + ] + privileged_commands.append("sleep 3") + privileged_commands.append( + "curl --preproxy socks5://localhost:1080" + " -s https://icanhazip.com" + ) + privileged_commands.append("sleep 1") + privileged_commands.append( + "launchctl bootstrap system" + " /Library/LaunchDaemons/app.meile.tun2socks.plist" + ) + privileged_commands.append("sleep 2") + privileged_commands.append( + "ifconfig utun123 198.18.0.1 198.18.0.1 up" + ) + + networks = [ + "1.0.0.0/8", + "2.0.0.0/7", + "4.0.0.0/6", + "8.0.0.0/5", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/1", + "198.18.0.0/15", + ] + + for network in networks: + privileged_commands.append( + f"route add -net {network} 198.18.0.1" + ) + + if not self.run_privileged_script(privileged_commands): + print("Failed to execute privileged commands") + return False + + return True + + def kill_daemon(self): + privileged_commands = [] + + networks = [ + "1.0.0.0/8", + "2.0.0.0/7", + "4.0.0.0/6", + "8.0.0.0/5", + "16.0.0.0/4", + "32.0.0.0/3", + "64.0.0.0/2", + "128.0.0.0/1", + "198.18.0.0/15", + ] + + for network in networks: + privileged_commands.append( + f"route delete -net {network} 198.18.0.1" + ) + + privileged_commands.append( + "ifconfig utun123 198.18.0.1 198.18.0.1 down" + ) + privileged_commands.append( + "launchctl bootout system" + " /Library/LaunchDaemons/app.meile.xray.plist" + " ; launchctl bootout system" + " /Library/LaunchDaemons/app.meile.tun2socks.plist" + ) + + self.run_privileged_script(privileged_commands) + + for proc in self.processes: + proc.terminate() + + return True + +# --------------------------------------------------------------------------- +# Select the correct handler for the current platform +# --------------------------------------------------------------------------- +if sys.platform.startswith('linux'): + V2RayHandler = _LinuxV2RayHandler +elif sys.platform == 'win32': + V2RayHandler = _WindowsV2RayHandler +elif sys.platform == 'darwin': + V2RayHandler = _DarwinV2RayHandler +else: + raise RuntimeError( + f"Unsupported platform: {sys.platform}" + ) + +# --------------------------------------------------------------------------- +# Configuration dataclasses – identical across all three platforms +# --------------------------------------------------------------------------- + @dataclass class V2RayFragmentConfiguration: api_port: int @@ -91,111 +471,109 @@ def get(self) -> dict: "tag": "proxy" } ], - "log": { - "loglevel": "none" - }, - "outbounds": [ - { - "mux": { - "concurrency": -1, - "enabled": False - }, - "protocol": self.proxy_protocol, - "settings": { - "vnext": [ - { - "address": self.vmess_address, - "port": self.vmess_port, - "users": [ - { - "alterId": 0, - "id": self.vmess_uid, - "level" : 8, - "security": "chacha20-poly1305" - } - ] - } - ] - }, - "streamSettings": { - "grpcSettings": { - "authority": "", - "health_check_timeout": 20, - "idle_timeout": 60, - "multiMode": False, - "serviceName": "" - }, - "network": self.vmess_transport, - "sockopt": { - "dialerProxy": "fragment", - "tcpKeepAliveIdle": 100, - "tcpNoDelay": True - } - }, - "tag": "vmess" + "log": { + "loglevel": "none" }, - { - "tag": "fragment", - "protocol": "freedom", - "settings": { - "domainStrategy": "AsIs", - "fragment": { - "packets": "1-3", - "length": "1-3", - "interval": "2-8" + "outbounds": [ + { + "mux": { + "concurrency": -1, + "enabled": False + }, + "protocol": self.proxy_protocol, + "settings": { + "vnext": [ + { + "address": self.vmess_address, + "port": self.vmess_port, + "users": [ + { + "alterId": 0, + "id": self.vmess_uid, + "level": 8, + "security": + "chacha20-poly1305" + } + ] + } + ] + }, + "streamSettings": { + "grpcSettings": { + "authority": "", + "health_check_timeout": 20, + "idle_timeout": 60, + "multiMode": False, + "serviceName": "" + }, + "network": self.vmess_transport, + "sockopt": { + "dialerProxy": "fragment", + "tcpKeepAliveIdle": 100, + "tcpNoDelay": True + } + }, + "tag": "vmess" + }, + { + "tag": "fragment", + "protocol": "freedom", + "settings": { + "domainStrategy": "AsIs", + "fragment": { + "packets": "1-3", + "length": "1-3", + "interval": "2-8" + } + }, + "streamSettings": { + "sockopt": { + "tcpKeepAliveIdle": 100, + "tcpNoDelay": True + } + } + }, + { + "protocol": "freedom", + "settings": { + "domainStrategy": "UseIP" + }, + "tag": "direct" + }, + { + "protocol": "blackhole", + "settings": { + "response": { + "type": "http" + } + }, + "tag": "block" } - }, - "streamSettings": { - "sockopt": { - "tcpKeepAliveIdle": 100, - "tcpNoDelay": True + ], + "policy": { + "levels": { + "0": { + "downlinkOnly": 0, + "uplinkOnly": 0 + } + }, + "system": { + "statsOutboundDownlink": True, + "statsOutboundUplink": True } - } }, - { - "protocol": "freedom", - "settings": { - "domainStrategy": "UseIP" - }, - "tag": "direct" - }, - { - "protocol": "blackhole", - "settings": { - "response": { - "type": "http" - } - }, - "tag": "block" - } - ], - "policy": { - "levels": { - "0": { - "downlinkOnly": 0, - "uplinkOnly": 0 - } + "routing": { + "rules": [ + { + "inboundTag": ["api"], + "outboundTag": "api", + "type": "field" + } + ] }, - "system": { - "statsOutboundDownlink": True, - "statsOutboundUplink": True - } - }, - "routing": { - "rules": [ - { - "inboundTag": ["api"], - "outboundTag": "api", - "type": "field" - } - ] - }, - "stats": {} + "stats": {} } - - - @dataclass class V2RayConfiguration: api_port: int From 96a63656123d358189e4f9a46bcae5fc1ffb4052 Mon Sep 17 00:00:00 2001 From: freqnik Date: Tue, 7 Apr 2026 15:29:16 -0400 Subject: [PATCH 11/12] update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 454c648..b19ebe4 100755 --- a/requirements.txt +++ b/requirements.txt @@ -57,6 +57,7 @@ mapview==1.0.6 mnemonic==0.21 more-itertools==10.2.0 mospy-wallet==0.6.0 +packaging==26.0 pexpect==4.8.0 Pillow==9.2.0 proto-plus==1.26.1 From c1b18ab85b98d43be597b5274e258ef786f88e23 Mon Sep 17 00:00:00 2001 From: freqnik Date: Tue, 7 Apr 2026 21:08:21 -0400 Subject: [PATCH 12/12] Make Linux specific changes --- src/bin/routes.sh | 2 +- src/cli/wallet.py | 78 ++++++++++++++++++++-------------------- src/kv/meile.kv | 6 ++-- src/typedef/konstants.py | 30 +++++++++------- src/ui/screens.py | 14 +++++--- src/utils/qr.py | 11 +++--- 6 files changed, 75 insertions(+), 66 deletions(-) diff --git a/src/bin/routes.sh b/src/bin/routes.sh index 89de0e9..1b3a9eb 100755 --- a/src/bin/routes.sh +++ b/src/bin/routes.sh @@ -26,7 +26,7 @@ if [[ ${STATE} = "up" ]]; then # get v2ray proxy IP PROXY_IP=`cat /home/${USER}/.meile-gui/v2ray.proxy` #echo ${PROXY_IP} > /home/${USER}/.meile-gui/v2ray.proxy - echo ${PROXY_IP} + echo "Proxy IP: ${PROXY_IP}" sleep 2 # add tun interface diff --git a/src/cli/wallet.py b/src/cli/wallet.py index 45c85d1..618eeba 100644 --- a/src/cli/wallet.py +++ b/src/cli/wallet.py @@ -7,7 +7,7 @@ import re import platform from time import sleep -from os import path, remove +from os import path, remove, chdir from urllib.parse import urlparse from urllib3.exceptions import NewConnectionError from grpc import RpcError, StatusCode @@ -1373,51 +1373,51 @@ def connect(self, # >> hardcoded = proxy port >> 1080 # >> hardcoded = v2ray file >> /home/${USER}/.meile-gui/v2ray_config.json - tuniface = False - v2ray_handler = V2RayHandler(f"{v2ray_tun2routes_connect_bash} up") - v2ray_handler.start_daemon() - sleep(14) + tuniface = False + v2ray_handler = V2RayHandler(f"{v2ray_tun2routes_connect_bash} up") + v2ray_handler.start_daemon() + sleep(14) - if pltfrm != Arch.OSX: - for iface in psutil.net_if_addrs().keys(): - if "tun" in iface: - tuniface = True - break - else: - if psutil.net_if_addrs().get("utun123"): - self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": True, "status" : "utun123"} - print(self.connected) + if pltfrm != Arch.OSX: + for iface in psutil.net_if_addrs().keys(): + if "tun" in iface: tuniface = True - - if tuniface is True: - self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": True, "status" : tuniface} + break + else: + if psutil.net_if_addrs().get("utun123"): + self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": True, "status" : "utun123"} print(self.connected) - conndesc.write("Checking network connection...\n") + tuniface = True + + if tuniface is True: + self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": True, "status" : tuniface} + print(self.connected) + conndesc.write("Checking network connection...\n") + conndesc.flush() + sleep(1) + self.get_ip_address() + sleep(1) + conndesc.close() + # os x + chdir(MeileConfig.BASEDIR) + return + else: + try: + conndesc.write("Error connecting to V2Ray node...\n") conndesc.flush() - sleep(1) - self.get_ip_address() - sleep(1) + v2ray_handler.v2ray_script = f"{v2ray_tun2routes_connect_bash} down" + v2ray_handler.kill_daemon() conndesc.close() - # os x - #chdir(MeileConfig.BASEDIR) - return - else: - try: - conndesc.write("Error connecting to V2Ray node...\n") - conndesc.flush() - v2ray_handler.v2ray_script = f"{v2ray_tun2routes_connect_bash} down" - v2ray_handler.kill_daemon() - conndesc.close() - except Exception as e: - print(str(e)) + except Exception as e: + print(str(e)) - self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": False, "status": f"Error connecting to v2ray node: {tuniface}"} - print(self.connected) - # os x - #chdir(MeileConfig.BASEDIR) - return + self.connected = {"v2ray_pid" : v2ray_handler.v2ray_pid, "result": False, "status": f"Error connecting to v2ray node: {tuniface}"} + print(self.connected) + # os x + chdir(MeileConfig.BASEDIR) + return # os x - #chdir(MeileConfig.BASEDIR) + chdir(MeileConfig.BASEDIR) self.connected = {"v2ray_pid" : None, "result": False, "status": "Bad Response from Node"} return diff --git a/src/kv/meile.kv b/src/kv/meile.kv index 13f558f..95de173 100755 --- a/src/kv/meile.kv +++ b/src/kv/meile.kv @@ -1116,7 +1116,7 @@ WindowManager: adaptive_height: True padding: [15, 25, 0, 25] AsyncImage: - source: "../imgs/plans_basic.png" + source: root.get_plan_image("b") size_hint: 1, None size: dp(215), dp(396) pos_hint: {"center_x": 0.5} @@ -1124,7 +1124,7 @@ WindowManager: keep_ratio: True padding: [15, 10, 15, 10] AsyncImage: - source: "../imgs/plans_premium.png" + source: root.get_plan_image("p") size_hint: 1, None size: dp(225), dp(396) pos_hint: {"center_x": 0.5} @@ -3055,4 +3055,4 @@ WindowManager: Check: group: 'proto' on_active: root.select_share_type(self, self.active, "v2") - pos_hint: {"x": .5, "y" : .025} \ No newline at end of file + pos_hint: {"x": .5, "y" : .025} diff --git a/src/typedef/konstants.py b/src/typedef/konstants.py index 7dd8396..c94f46e 100755 --- a/src/typedef/konstants.py +++ b/src/typedef/konstants.py @@ -603,7 +603,7 @@ class IBCTokens(): #mu_coins = ["tsent", "udvpn", "uscrt", "uosmo", "uatom", "udec"] class TextStrings(): dash = "-" - VERSION = "v2.5.1" + VERSION = "v2.5.4" BUILD = "17748518213" RootTag = "SENTINEL" GITHUB_API_URL = "https://api.github.com/repos/MathNodes/meile-gui/releases/latest" @@ -623,17 +623,23 @@ class MeileColors(): ROW_HOVER = "#39363c" DOWNLOAD = "#2fe548" UPLOAD = "#2fa1e5" - FONT_FACE = "fonts/mplus-2c-bold.ttf" - FONT_FACE_ARIAL = "fonts/arial-unicode-ms.ttf" - QR_FONT_FACE = "fonts/Roboto-BoldItalic.ttf" - MAP_MARKER = "imgs/location_pin.png" - LOC_MARKER = "imgs/location_marker.png" - LOGO = "imgs/logo.png" - LOGO_HD = "imgs/logo_hd.png" - LOGO_TEXT = "imgs/logo_text.png" - SUBSCRIBE_BUTTON = "imgs/SubscribeButton.png" - GETINFO_BUTTON = "imgs/GetInfoButton.png" - SPINNER = "imgs/spinner.png" + FONT_FACE = "../fonts/mplus-2c-bold.ttf" + FONT_FACE_ARIAL = "../fonts/arial-unicode-ms.ttf" + QR_FONT_FACE = "../fonts/Roboto-BoldItalic.ttf" + MAP_MARKER = "../imgs/location_pin.png" + LOC_MARKER = "../imgs/location_marker.png" + LOGO = "../imgs/logo.png" + LOGO_HD = "../imgs/logo_hd.png" + LOGO_TEXT = "../imgs/logo_text.png" + SUBSCRIBE_BUTTON = "../imgs/SubscribeButton.png" + GETINFO_BUTTON = "../imgs/GetInfoButton.png" + SPINNER = "../imgs/spinner.png" + CONNECT_BUTTON = "../imgs/ConnectButton.png" + DISCONNECT_BUTTON = "../imgs/DisconnectButton.png" + WIREGUARD_ICON = "../utils/coinimg/wireguard.png" + V2RAY_ICON = "../utils/coinimg/v2ray.png" + BASIC_PLAN = "../imgs/plans_basic.png" + PREMIUM_PLAN = "../imgs/plans_premium.png" HEALTH_ICON = "shield-plus" SICK_ICON = "emoticon-sick" ARCGIS_MAP = "https://server.arcgisonline.com/arcgis/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}.png" diff --git a/src/ui/screens.py b/src/ui/screens.py index a02558e..a5c4c39 100644 --- a/src/ui/screens.py +++ b/src/ui/screens.py @@ -1677,11 +1677,9 @@ def remove_loading_widget2(self): def return_connect_button(self, text): MeileConfig = MeileGuiConfig() if text == "c": - button_path = "../imgs/ConnectButton.png" - return MeileConfig.resource_path(button_path) + return MeileConfig.resource_path(MeileColors.CONNECT_BUTTON) else: - button_path = "../imgs/DisconnectButton.png" - return MeileConfig.resource_path(button_path) + return MeileConfig.resource_path(MeileColors.DISCONNECT_BUTTON) def closeDialog(self, inst): try: @@ -2358,6 +2356,14 @@ def set_previous_screen(self): self.mw.carousel.remove_widget(self.mw.NodeWidget) self.mw.carousel.load_previous() + def get_plan_image(self, plan_type): + MeileConfig = MeileGuiConfig() + if plan_type == "b": + return MeileConfig.resource_path(MeileColors.BASIC_PLAN) + else: + return MeileConfig.resource_path(MeileColors.PREMIUM_PLAN) + + def finished(self, *args): self.ids.rv.remove_widget(self.label) self.ids.rv.remove_widget(self.spinner) diff --git a/src/utils/qr.py b/src/utils/qr.py index c754a2f..597803e 100755 --- a/src/utils/qr.py +++ b/src/utils/qr.py @@ -8,6 +8,7 @@ import hashlib from helpers.helpers import is_ecryptfs_mounted from conf.meile_config import MeileGuiConfig +from typedef.konstants import MeileColors class QRCode(): @@ -32,9 +33,7 @@ def generate_wg_qr_code(self, conf_path, label=None): label = path.basename(conf_path) - wg_logo_path = self.MeileConfig.resource_path( - 'utils/coinimg/wireguard.png' - ) + wg_logo_path = self.MeileConfig.resource_path(MeileColors.WIREGUARD_ICON) has_logo = path.exists(wg_logo_path) QRcode = qrcode.QRCode( @@ -77,9 +76,7 @@ def generate_wg_qr_code(self, conf_path, label=None): (255, 255, 255, 255), ) robotoFont = ImageFont.truetype( - self.MeileConfig.resource_path( - 'utils/fonts/Roboto-BoldItalic.ttf' - ), + self.MeileConfig.resource_path(MeileColors.QR_FONT_FACE), fontSize, ) @@ -163,4 +160,4 @@ def generate_qr_code(self, ADDRESS, coin): background.save(path.join(self.IMGDIR, ADDRESS + ".png")) return path.join(self.IMGDIR, ADDRESS + ".png") - \ No newline at end of file +