From 22be3b6067ceea3c23e79ae7f5f891c7b22bd551 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 5 Jan 2024 01:49:25 +0100 Subject: [PATCH] Added multilingual text rendering --- sbapp/buildozer.spec | 4 +- sbapp/main.py | 24 ++++++- sbapp/patches/AndroidManifest.tmpl.xml | 2 + sbapp/ui/helpers.py | 89 ++++++++++++++++++++++++++ sbapp/ui/messages.py | 18 +++++- 5 files changed, 131 insertions(+), 6 deletions(-) diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index b7ea8e5..78dd030 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -5,12 +5,12 @@ package.domain = io.unsigned source.dir = . source.include_exts = py,png,jpg,jpeg,webp,ttf,kv,pyi,typed,so,0,1,2,3,atlas,frag,html,css,js,whl,zip,gz,woff2,pdf,epub -source.include_patterns = assets/*,share/* +source.include_patterns = assets/*,assets/fonts/*,share/* source.exclude_patterns = app_storage/*,venv/*,Makefile,./Makefil*,requirements,precompiled/*,parked/*,./setup.py,Makef*,./Makefile,Makefile version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py -android.numeric_version = 20231207 +android.numeric_version = 20240103 # Cryptography recipe is currently broken, using RNS-internal crypto for now requirements = kivy==2.2.1,libbz2,pillow,qrcode==7.3.1,usb4a,usbserial4a diff --git a/sbapp/main.py b/sbapp/main.py index d58003b..dfedba6 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1,6 +1,6 @@ __debug_build__ = False __disable_shaders__ = False -__version__ = "0.7.7" +__version__ = "0.7.8" __variant__ = "beta" import sys @@ -179,6 +179,7 @@ class SidebandApp(MDApp): self.sideband = SidebandCore(self, is_client=False, verbose=(args.verbose or __debug_build__)) self.set_ui_theme() + self.font_config() self.dark_theme_text_color = dark_theme_text_color self.conversations_view = None @@ -347,6 +348,27 @@ class SidebandApp(MDApp): self.update_ui_colors() + def font_config(self): + from kivy.core.text import LabelBase, DEFAULT_FONT + fb_path = "assets/fonts/" + LabelBase.register(name="hebrew", + fn_regular=fb_path+"NotoSansHebrew-Regular.ttf", + fn_bold=fb_path+"NotoSansHebrew-Bold.ttf",) + + LabelBase.register(name="japanese", + fn_regular=fb_path+"NotoSansJP-Regular.ttf") + + LabelBase.register(name="chinese", + fn_regular=fb_path+"NotoSansSC-Regular.ttf") + + LabelBase.register(name="korean", + fn_regular=fb_path+"NotoSansKR-Regular.ttf") + + LabelBase.register(name="emoji", + fn_regular=fb_path+"NotoEmoji-Regular.ttf") + + + def update_ui_colors(self): if self.sideband.config["dark_ui"]: self.color_reject = colors["DeepOrange"]["900"] diff --git a/sbapp/patches/AndroidManifest.tmpl.xml b/sbapp/patches/AndroidManifest.tmpl.xml index 5a4f128..140814c 100644 --- a/sbapp/patches/AndroidManifest.tmpl.xml +++ b/sbapp/patches/AndroidManifest.tmpl.xml @@ -27,6 +27,8 @@ + + diff --git a/sbapp/ui/helpers.py b/sbapp/ui/helpers.py index 2ef4942..5957982 100644 --- a/sbapp/ui/helpers.py +++ b/sbapp/ui/helpers.py @@ -31,3 +31,92 @@ class DrawerList(MDList): class IconListItem(OneLineIconListItem): icon = StringProperty() +def is_emoji(unicode_character): + return unicode_character in emoji_lookup + +def multilingual_markup(data): + # TODO: Remove + # import time + # ts = time.time() + + do = "" + rfont = "default" + ds = data.decode("utf-8") + for cp in ds: + match = False + switch = False + pfont = rfont + + if is_emoji(cp): + match = True + if rfont != "emoji": + switch = True + rfont = "emoji" + + if not match: + for range_start in codepoint_map: + range_end = codepoint_map[range_start][0] + mapped_font = codepoint_map[range_start][1] + + if range_end >= ord(cp) >= range_start: + match = True + if rfont != mapped_font: + rfont = mapped_font + switch = True + break + + if (not match) and rfont != "default": + rfont = "default" + switch = True + + if switch: + if pfont != "default": + do += "[/font]" + if rfont != "default": + do += "[font="+str(rfont)+"]" + + do += cp + + if rfont != "default": + do += "[/font]" + + # TODO: Remove + # print(do+"\n\n"+str(time.time()-ts)) + + return do.encode("utf-8") + +codepoint_map = { + 0x0590: [0x05ff, "hebrew"], + 0x2e3a: [0x2e3b, "chinese"], + 0x2e80: [0x2ef3, "chinese"], + 0x2f00: [0x2fdf, "chinese"], + 0x2ff0: [0x2fff, "chinese"], + 0x3000: [0x303f, "chinese"], + 0x3040: [0x309f, "japanese"], + 0x30a0: [0x30ff, "japanese"], + 0x3100: [0x312f, "japanese"], + 0x3130: [0x318f, "japanese"], + 0x3190: [0x319f, "japanese"], + 0x31a0: [0x31bf, "japanese"], + 0x31c0: [0x31ef, "japanese"], + 0x31f0: [0x31ff, "japanese"], + 0x3200: [0x32ff, "japanese"], + 0x3300: [0x33ff, "japanese"], + 0xf900: [0xfa6d, "japanese"], + 0xfb00: [0xfb04, "japanese"], + 0xfe10: [0xfe1f, "japanese"], + 0xfe30: [0xfe4f, "japanese"], + 0xfe50: [0xfe6f, "japanese"], + 0x3400: [0x4dbf, "chinese"], + 0x4e00: [0x9fff, "chinese"], + 0xff00: [0xffef, "japanese"], + 0x10000: [0x1fffd, "japanese"], + 0x1f100: [0x1f1ff, "japanese"], + 0x1f200: [0x1f2ff, "japanese"], + 0x20000: [0x3fffd, "japanese"], + 0xa960: [0xa97f, "korean"], + 0xac00: [0xd7af, "korean"], + 0xd7b0: [0xd7ff, "korean"], +} + +emoji_lookup = ['⌚','⌛','','⏪','⏫','⏬','⏰','⏳','◽','◾','☔','☕','♈','♉','♊','♋','♌','♍','♎','♏','♐','♑','♒','♓','♿','⚓','⚡','⚪','⚫','⚽','⚾','⛄','⛅','⛎','⛔','⛪','⛲','⛳','⛵','⛺','⛽','✅','✊','✋','✨','❌','❎','❓','❔','❕','❗','➕','➖','➗','➰','➿','⬛','⬜','⭐','⭕','🀄','🃏','🆎','🆑','🆒','🆓','🆔','🆕','🆖','🆗','🆘','🆙','🆚','🈁','🈚','🈯','🈲','🈳','🈴','🈵','🈶','🈸','🈹','🈺','🉐','🉑','🌀','🌁','🌂','🌃','🌄','🌅','🌆','🌇','🌈','🌉','🌊','🌋','🌌','🌍','🌎','🌏','🌐','🌑','🌒','🌓','🌔','🌕','🌖','🌗','🌘','🌙','🌚','🌛','🌜','🌝','🌞','🌟','🌠','🌭','🌮','🌯','🌰','🌱','🌲','🌳','🌴','🌵','🌷','🌸','🌹','🌺','🌻','🌼','🌽','🌾','🌿','🍀','🍁','🍂','🍃','🍄','🍅','🍆','🍇','🍈','🍉','🍊','🍋','🍌','🍍','🍎','🍏','🍐','🍑','🍒','🍓','🍔','🍕','🍖','🍗','🍘','🍙','🍚','🍛','🍜','🍝','🍞','🍟','🍠','🍡','🍢','🍣','🍤','🍥','🍦','🍧','🍨','🍩','🍪','🍫','🍬','🍭','🍮','🍯','🍰','🍱','🍲','🍳','🍴','🍵','🍶','🍷','🍸','🍹','🍺','🍻','🍼','🍾','🍿','🎀','🎁','🎂','🎃','🎄','🎅','🎆','🎇','🎈','🎉','🎊','🎋','🎌','🎍','🎎','🎏','🎐','🎑','🎒','🎓','🎠','🎡','🎢','🎣','🎤','🎥','🎦','🎧','🎨','🎩','🎪','🎫','🎬','🎭','🎮','🎯','🎰','🎱','🎲','🎳','🎴','🎵','🎶','🎷','🎸','🎹','🎺','🎻','🎼','🎽','🎾','🎿','🏀','🏁','🏂','🏃','🏄','🏅','🏆','🏇','🏈','🏉','🏊','🏏','🏐','🏑','🏒','🏓','🏠','🏡','🏢','🏣','🏤','🏥','🏦','🏧','🏨','🏩','🏪','🏫','🏬','🏭','🏮','🏯','🏰','🏴','🏸','🏹','🏺','🏻','🏼','🏽','🏾','🏿','🐀','🐁','🐂','🐃','🐄','🐅','🐆','🐇','🐈','🐉','🐊','🐋','🐌','🐍','🐎','🐏','🐐','🐑','🐒','🐓','🐔','🐕','🐖','🐗','🐘','🐙','🐚','🐛','🐜','🐝','🐞','🐟','🐠','🐡','🐢','🐣','🐤','🐥','🐦','🐧','🐨','🐩','🐪','🐫','🐬','🐭','🐮','🐯','🐰','🐱','🐲','🐳','🐴','🐵','🐶','🐷','🐸','🐹','🐺','🐻','🐼','🐽','🐾','👀','👂','👃','👄','👅','👆','👇','👈','👉','👊','👋','👌','👍','👎','👏','👐','👑','👒','👓','👔','👕','👖','👗','👘','👙','👚','👛','👜','👝','👞','👟','👠','👡','👢','👣','👤','👥','👦','👧','👨','👩','👪','👫','👬','👭','👮','👯','👰','👱','👲','👳','👴','👵','👶','👷','👸','👹','👺','👻','👼','👽','👾','👿','💀','💁','💂','💃','💄','💅','💆','💇','💈','💉','💊','💋','💌','💍','💎','💏','💐','💑','💒','💓','💔','💕','💖','💗','💘','💙','💚','💛','💜','💝','💞','💟','💠','💡','💢','💣','💤','💥','💦','💧','💨','💩','💪','💫','💬','💭','💮','💯','💰','💱','💲','💳','💴','💵','💶','💷','💸','💹','💺','💻','💼','💽','💾','💿','📀','📁','📂','📃','📄','📅','📆','📇','📈','📉','📊','📋','📌','📍','📎','📏','📐','📑','📒','📓','📔','📕','📖','📗','📘','📙','📚','📛','📜','📝','📞','📟','📠','📡','📢','📣','📤','📥','📦','📧','📨','📩','📪','📫','📬','📭','📮','📯','📰','📱','📲','📳','📴','📵','📶','📷','📸','📹','📺','📻','📼','📿','🔀','🔁','🔂','🔃','🔄','🔅','🔆','🔇','🔈','🔉','🔊','🔋','🔌','🔍','🔎','🔏','🔐','🔑','🔒','🔓','🔔','🔕','🔖','🔗','🔘','🔙','🔚','🔛','🔜','🔝','🔞','🔟','🔠','🔡','🔢','🔣','🔤','🔥','🔦','🔧','🔨','🔩','🔪','🔫','🔬','🔭','🔮','🔯','🔰','🔱','🔲','🔳','🔴','🔵','🔶','🔷','🔸','🔹','🔺','🔻','🔼','🔽','🕋','🕌','🕍','🕎','🕐','🕑','🕒','🕓','🕔','🕕','🕖','🕗','🕘','🕙','🕚','🕛','🕜','🕝','🕞','🕟','🕠','🕡','🕢','🕣','🕤','🕥','🕦','🕧','🖕','🖖','🗻','🗼','🗽','🗾','🗿','😀','😁','😂','😃','😄','😅','😆','😇','😈','😉','😊','😋','😌','😍','😎','😏','😐','😑','😒','😓','😔','😕','😖','😗','😘','😙','😚','😛','😜','😝','😞','😟','😠','😡','😢','😣','😤','😥','😦','😧','😨','😩','😪','😫','😬','😭','😮','😯','😰','😱','😲','😳','😴','😵','😶','😷','😸','😹','😺','😻','😼','😽','😾','😿','🙀','🙁','🙂','🙃','🙄','🙅','🙆','🙇','🙈','🙉','🙊','🙋','🙌','🙍','🙎','🙏','🚀','🚁','🚂','🚃','🚄','🚅','🚆','🚇','🚈','🚉','🚊','🚋','🚌','🚍','🚎','🚏','🚐','🚑','🚒','🚓','🚔','🚕','🚖','🚗','🚘','🚙','🚚','🚛','🚜','🚝','🚞','🚟','🚠','🚡','🚢','🚣','🚤','🚥','🚦','🚧','🚨','🚩','🚪','🚫','🚬','🚭','🚮','🚯','🚰','🚱','🚲','🚳','🚴','🚵','🚶','🚷','🚸','🚹','🚺','🚻','🚼','🚽','🚾','🚿','🛀','🛁','🛂','🛃','🛄','🛅','🛌','🛐','🛫','🛬','🤐','🤑','🤒','🤓','🤔','🤕','🤖','🤗','🤘','🦀','🦁','🦂','🦃','🦄','🧀','🇦🇨','🇦🇩','🇦🇪','🇦🇫','🇦🇬','🇦🇮','🇦🇱','🇦🇲','🇦🇴','🇦🇶','🇦🇷','🇦🇸','🇦🇹','🇦🇺','🇦🇼','🇦🇽','🇦🇿','🇧🇦','🇧🇧','🇧🇩','🇧🇪','🇧🇫','🇧🇬','🇧🇭','🇧🇮','🇧🇯','🇧🇱','🇧🇲','🇧🇳','🇧🇴','🇧🇶','🇧🇷','🇧🇸','🇧🇹','🇧🇻','🇧🇼','🇧🇾','🇧🇿','🇨🇦','🇨🇨','🇨🇩','🇨🇫','🇨🇬','🇨🇭','🇨🇮','🇨🇰','🇨🇱','🇨🇲','🇨🇳','🇨🇴','🇨🇵','🇨🇷','🇨🇺','🇨🇻','🇨🇼','🇨🇽','🇨🇾','🇨🇿','🇩🇪','🇩🇬','🇩🇯','🇩🇰','🇩🇲','🇩🇴','🇩🇿','🇪🇦','🇪🇨','🇪🇪','🇪🇬','🇪🇭','🇪🇷','🇪🇸','🇪🇹','🇪🇺','🇫🇮','🇫🇯','🇫🇰','🇫🇲','🇫🇴','🇫🇷','🇬🇦','🇬🇧','🇬🇩','🇬🇪','🇬🇫','🇬🇬','🇬🇭','🇬🇮','🇬🇱','🇬🇲','🇬🇳','🇬🇵','🇬🇶','🇬🇷','🇬🇸','🇬🇹','🇬🇺','🇬🇼','🇬🇾','🇭🇰','🇭🇲','🇭🇳','🇭🇷','🇭🇹','🇭🇺','🇮🇨','🇮🇩','🇮🇪','🇮🇱','🇮🇲','🇮🇳','🇮🇴','🇮🇶','🇮🇷','🇮🇸','🇮🇹','🇯🇪','🇯🇲','🇯🇴','🇯🇵','🇰🇪','🇰🇬','🇰🇭','🇰🇮','🇰🇲','🇰🇳','🇰🇵','🇰🇷','🇰🇼','🇰🇾','🇰🇿','🇱🇦','🇱🇧','🇱🇨','🇱🇮','🇱🇰','🇱🇷','🇱🇸','🇱🇹','🇱🇺','🇱🇻','🇱🇾','🇲🇦','🇲🇨','🇲🇩','🇲🇪','🇲🇫','🇲🇬','🇲🇭','🇲🇰','🇲🇱','🇲🇲','🇲🇳','🇲🇴','🇲🇵','🇲🇶','🇲🇷','🇲🇸','🇲🇹','🇲🇺','🇲🇻','🇲🇼','🇲🇽','🇲🇾','🇲🇿','🇳🇦','🇳🇨','🇳🇪','🇳🇫','🇳🇬','🇳🇮','🇳🇱','🇳🇴','🇳🇵','🇳🇷','🇳🇺','🇳🇿','🇴🇲','🇵🇦','🇵🇪','🇵🇫','🇵🇬','🇵🇭','🇵🇰','🇵🇱','🇵🇲','🇵🇳','🇵🇷','🇵🇸','🇵🇹','🇵🇼','🇵🇾','🇶🇦','🇷🇪','🇷🇴','🇷🇸','🇷🇺','🇷🇼','🇸🇦','🇸🇧','🇸🇨','🇸🇩','🇸🇪','🇸🇬','🇸🇭','🇸🇮','🇸🇯','🇸🇰','🇸🇱','🇸🇲','🇸🇳','🇸🇴','🇸🇷','🇸🇸','🇸🇹','🇸🇻','🇸🇽','🇸🇾','🇸🇿','🇹🇦','🇹🇨','🇹🇩','🇹🇫','🇹🇬','🇹🇭','🇹🇯','🇹🇰','🇹🇱','🇹🇲','🇹🇳','🇹🇴','🇹🇷','🇹🇹','🇹🇻','🇹🇼','🇹🇿','🇺🇦','🇺🇬','🇺🇲','🇺🇸','🇺🇾','🇺🇿','🇻🇦','🇻🇨','🇻🇪','🇻🇬','🇻🇮','🇻🇳','🇻🇺','🇼🇫','🇼🇸','🇽🇰','🇾🇪','🇾🇹','🇿🇦','🇿🇲','🇿🇼']; \ No newline at end of file diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index 62cdc69..b5f7a87 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -12,10 +12,16 @@ from kivy.properties import StringProperty, BooleanProperty from kivy.uix.gridlayout import GridLayout from kivy.uix.boxlayout import BoxLayout from kivy.clock import Clock +from kivy.utils import escape_markup from kivymd.uix.button import MDRectangleFlatButton, MDRectangleFlatIconButton from kivymd.uix.dialog import MDDialog +if RNS.vendor.platformutils.get_platform() == "android": + from ui.helpers import multilingual_markup +else: + from .helpers import multilingual_markup + import os import plyer import subprocess @@ -175,6 +181,13 @@ class Messages(): for m in self.new_messages: if not m["hash"] in self.added_item_hashes: + if not self.is_trusted: + message_input = str( escape_markup(m["content"].decode("utf-8")) ).encode("utf-8") + else: + message_input = m["content"] + + message_markup = multilingual_markup(message_input) + txstr = time.strftime(ts_format, time.localtime(m["sent"])) rxstr = time.strftime(ts_format, time.localtime(m["received"])) titlestr = "" @@ -305,7 +318,7 @@ class Messages(): force_markup = True item = ListLXMessageCard( - text=pre_content+m["content"].decode("utf-8")+extra_content, + text=pre_content+message_markup.decode("utf-8")+extra_content, heading=heading_str, md_bg_color=msg_color, ) @@ -320,7 +333,6 @@ class Messages(): item.ids.content_text.text_color = mt_color item.ids.msg_submenu.theme_text_color = "Custom" item.ids.msg_submenu.text_color = mt_color - item.ids.content_text.markup = self.is_trusted or force_markup def gen_del(mhash, item): def x(): @@ -759,7 +771,7 @@ Builder.load_string(""" MDLabel: id: content_text text: root.text - markup: False + markup: True size_hint_y: None text_size: self.width, None height: self.texture_size[1]