├── README ├── large-letter-js.py ├── install-all.py ├── ruoat.yaml ├── gym-log ├── copypaste └── copypaste.py ├── remove-super.py ├── varauskalenteri.py ├── onedrive_dedup.py ├── asyncify.py ├── rounding.py ├── tilannehuone.py ├── custom-trainer.py ├── brython.py ├── gym-log-07-Jul-2020 ├── ruokaa.py ├── bigletter └── big-letter-service.py ├── kalkylation.py ├── gym-test-prog.yaml ├── threadbare.py ├── agenr.py ├── styleproperties.py ├── gym-program.yaml ├── vector.py ├── objc_plus 2.py ├── objc_plus.py ├── gym-list.py ├── wikipedia-browser.py ├── helmet-list.py ├── timer 2.py ├── timer.py ├── pic2text.py ├── gym.py ├── gym 2.py ├── gym-07-Jul-2020.py ├── sfsymbol 2.py ├── sfsymbol.py ├── randomseat.py ├── uiutils 2.py ├── jswrapper.py ├── uiutils.py └── dashboard.py /README: -------------------------------------------------------------------------------- 1 | Miscellaneous small projects -------------------------------------------------------------------------------- /large-letter-js.py: -------------------------------------------------------------------------------- 1 | import wkwebview 2 | -------------------------------------------------------------------------------- /install-all.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | links = [ 4 | 5 | ] 6 | -------------------------------------------------------------------------------- /ruoat.yaml: -------------------------------------------------------------------------------- 1 | - Paistettu riisi 2 | - Kalapuikot 3 | - Kasviskeitto 4 | - Salaatti 5 | - Mozzarellasalaatti 6 | - Kanalaatikko 7 | - Hunajakana 8 | - Tortilla kaikilla täytteillä 9 | - Porkkanasoppa 10 | - Pinaattikeitto 11 | - Pasta Bolognese 12 | - Pinaattipannari 13 | - Lihapullat 14 | -------------------------------------------------------------------------------- /gym-log: -------------------------------------------------------------------------------- 1 | 2019-08-05 Mikael Pohkeita 20 2 | 2019-08-05 Oskari Pohkeita 0 3 | 2019-08-05 Mikael Reisikääntö 50 4 | 2019-08-05 Oskari Reisikääntö 32 5 | 2019-08-05 Mikael Kyykky_penkille 20 6 | 2019-08-05 Oskari Kyykky_penkille 8 7 | 2019-08-05 Mikael Reisien_ojentajat 32 8 | 2019-08-05 Oskari Reisien_ojentajat 14 9 | -------------------------------------------------------------------------------- /copypaste/copypaste.py: -------------------------------------------------------------------------------- 1 | clip = None 2 | 3 | def handler(event, context): 4 | ''' 5 | @awslambda 6 | ''' 7 | global clip 8 | 9 | clip_in = event['queryParams'].get('clip', None) 10 | if clip_in: 11 | clip = clip_in 12 | return clip_in 13 | else: 14 | clip_out = clip 15 | clip = None 16 | return clip_out 17 | 18 | ''' 19 | Windows: 20 | 21 | Copy to cloud: 22 | powershell -sta "add-type -as System.Windows.Forms; $text_to_copy = [uri]::EscapeDataString([windows.forms.clipboard]::GetText()); (Invoke-WebRequest """https://uxyv0k54q5.execute-api.eu-west-1.amazonaws.com/prod/?clip=$text_to_copy""").Content" 23 | 24 | Copy from cloud: 25 | powershell -sta "Write-Output (Invoke-WebRequest """https://uxyv0k54q5.execute-api.eu-west-1.amazonaws.com/prod/""").Content | Set-Clipboard" 26 | ''' 27 | -------------------------------------------------------------------------------- /remove-super.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | from urllib.parse import urlparse 4 | from pprint import pprint 5 | 6 | import appex 7 | 8 | import spoter 9 | import spotify_ids 10 | 11 | url = appex.get_url() 12 | url = 'https://open.spotify.com/track/1ueOEwIxlCAivSC8ZrA5xW?si=UX8sqKi-RgeUk_Vn40-N1g' 13 | 14 | playlist_name = sys.argv[1] 15 | 16 | track_id = os.path.basename(urlparse(url).path) 17 | print(track_id) 18 | 19 | spot = spoter.Spoter( 20 | spotify_ids.client_id, spotify_ids.client_secret 21 | ) 22 | 23 | print(spot.track(track_id)) 24 | 25 | the_playlist = None 26 | playlists = spot.get_all('items', spot.user_playlists, limit=50) 27 | for playlist in playlists: 28 | print(playlist['name']) 29 | if playlist['name'] == playlist_name: 30 | the_playlist = playlist 31 | break 32 | else: 33 | print(f'Playlist {playlist_name} not found') 34 | 35 | if the_playlist: 36 | pprint(the_playlist) 37 | positions = [] 38 | for i, track in enumerate(spot.get_all('items', 39 | spot.playlist_tracks, the_playlist, limit=50)): 40 | if track['track']['id'] == track_id: 41 | positions.append(i) 42 | 43 | spot.delete_tracks_from_playlist(the_playlist, [(track_id, positions)]) 44 | -------------------------------------------------------------------------------- /varauskalenteri.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | import dateutil.easter 3 | from dateutil.relativedelta import relativedelta, SA as Saturday 4 | 5 | from typing import Tuple 6 | 7 | 8 | names = ['Alfa', 'Beta', 'Gamma', 'Delta', 'Epsilon'] 9 | 10 | name_order_filename = 'names.txt' 11 | 12 | 13 | def week_from_date(date): 14 | _, week, _ = date.isocalendar() 15 | return week 16 | 17 | def holiday_weeks(year: int) -> Tuple[int, int]: 18 | """ 19 | Palauttaa pääsiäis- ja juhannusviikkojen numerot pyydetylle vuodelle. 20 | 21 | >>> holiday_weeks(year=2020) 22 | (15, 25) 23 | >>> holiday_weeks(year=2025) 24 | (16, 25) 25 | """ 26 | easter_week = week_from_date(dateutil.easter.easter(year)) 27 | midsummer_week = week_from_date(date(year, 6, 20) + relativedelta(weekday=Saturday)) 28 | 29 | return easter_week, midsummer_week 30 | 31 | def weeks_per_year(year): 32 | """ 33 | >>> weeks_per_year(2020) 34 | 53 35 | >>> weeks_per_year(2021) 36 | 52 37 | """ 38 | return week_from_date(date(year, 12, 31)) 39 | 40 | def pick_midsummer_turn(all_names, already_had): 41 | return random.choice(all_names, already_had) 42 | 43 | def distribute_weeks(year): 44 | ... 45 | 46 | 47 | -------------------------------------------------------------------------------- /onedrive_dedup.py: -------------------------------------------------------------------------------- 1 | import ui 2 | 3 | import onedriver 4 | import onedrive_ids 5 | 6 | driver = onedriver.OneDriver( 7 | onedrive_ids.client_id, 8 | onedrive_ids.client_secret) 9 | 10 | def select_which_to_delete(one, two): 11 | one_name = one['name'] 12 | one_path = one['parentReference']['path'][len('/drive/root:'):] 13 | two_name = two['name'] 14 | two_path = two['parentReference']['path'][len('/drive/root:'):] 15 | print('1', one_path, one_name) 16 | print('2', two_path, two_name) 17 | while True: 18 | response = input('1 or 2 to delete, s to skip, q to quit') 19 | if response not in ['1', '2', 's', 'q']: continue 20 | if response == '1': 21 | print('Deleting') 22 | return one 23 | elif response == '2': 24 | print('Deleting') 25 | return two 26 | elif response == 's': 27 | print('Skipping') 28 | return None 29 | elif response == 'q': 30 | print('Stopping') 31 | raise StopIteration() 32 | 33 | def print_path(folder, full_path): 34 | print(full_path) 35 | 36 | driver.deduplicate('Paperit', 37 | decision_callback=select_which_to_delete, 38 | info_callback=print_path) 39 | 40 | -------------------------------------------------------------------------------- /asyncify.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import bottle, threading, requests, functools 3 | 4 | def wrap(obj, port=8080, quiet=True): 5 | server = MyWSGIRefServer(port=port) 6 | app = Proxy() 7 | 8 | threading.Thread(group=None, target=app.run, name=None, args=(), kwargs={'server': server, 'quiet': quiet}).start() 9 | 10 | 11 | class _MyWSGIRefServer(bottle.ServerAdapter): 12 | server = None 13 | 14 | def run(self, handler): 15 | from wsgiref.simple_server import make_server, WSGIRequestHandler 16 | if self.quiet: 17 | class QuietHandler(WSGIRequestHandler): 18 | def log_request(*args, **kw): pass 19 | self.options['handler_class'] = QuietHandler 20 | self.server = make_server(self.host, self.port, handler, **self.options) 21 | self.server.serve_forever() 22 | 23 | def stop(self): 24 | self.server.shutdown() 25 | 26 | 27 | class _ProxyApp(bottle.Bottle): 28 | 29 | def __init__(self, *args, **kwargs): 30 | pass 31 | 32 | 33 | class _ProxyClient: 34 | 35 | def __init__(self, obj): 36 | self._obj = obj 37 | 38 | def call_method(self, name, func, *args, **kwargs): 39 | return func(*args, **kwargs) 40 | 41 | def __getattr__(self, key, go=object.__getattribute__): 42 | target = getattr(go(self, '_obj'), key) 43 | return functools.partial(go(self, 'call_method'), key, target) 44 | 45 | if __name__ == '__main__': 46 | l = ['a'] 47 | pl = _ProxyClient(l) 48 | pl.append('b') 49 | print(l) 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /rounding.py: -------------------------------------------------------------------------------- 1 | import decimal, json 2 | from pydantic import BaseModel, conlist 3 | from typing import List 4 | import itertools, math 5 | 6 | assert decimal.getcontext().rounding == decimal.ROUND_HALF_EVEN 7 | 8 | n = decimal.Decimal('2.5') 9 | 10 | 11 | incoming = { 12 | 'invoice_lines': [ 13 | { 14 | 'description': 'Holvi T-shirt', 15 | 'quantity': 2, 16 | 'category': 'Clothes', 17 | 'unit_price_net': '25.00' 18 | }, 19 | { 20 | 'description': 'Holvi hoodie', 21 | 'quantity': 1, 22 | 'category': 'Clothes', 23 | 'unit_price_net': '40.00' 24 | }, 25 | { 26 | 'description': 'Holvi poster', 27 | 'quantity': 4, 28 | 'category': 'Posters', 29 | 'unit_price_net': '40.00' 30 | } 31 | ], 32 | 'payments': [ 33 | { 34 | 'id': 1, 35 | 'amount': '50.00' 36 | }, 37 | { 38 | 'id': 2, 39 | 'amount': '200.00' 40 | } 41 | ] 42 | } 43 | 44 | 45 | class InvoiceLine(BaseModel): 46 | description: str 47 | quantity: int 48 | category: str 49 | unit_price_net: decimal.Decimal 50 | 51 | class Payment(BaseModel): 52 | id: int 53 | amount: decimal.Decimal 54 | 55 | class Request(BaseModel): 56 | invoice_lines: conlist(InvoiceLine, min_items=1) 57 | payments: conlist(Payment, min_items=1) 58 | 59 | class Categorisation(BaseModel): 60 | category: str 61 | net_amount: decimal.Decimal 62 | 63 | class Categorisations(BaseModel): 64 | id: int 65 | categorisations: List[Categorisation] 66 | 67 | 68 | request = Request(**incoming) 69 | 70 | print(request) 71 | 72 | rounding_options = [math.floor, math.ceil] 73 | 74 | c = itertools.combinations_with_replacement(rounding_options, 5) 75 | 76 | for d in c: 77 | print(d) 78 | -------------------------------------------------------------------------------- /tilannehuone.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | import requests, bs4 4 | import notification 5 | 6 | r = requests.get('http://www.tilannehuone.fi/halytys.php') 7 | 8 | soup = bs4.BeautifulSoup(r.content.decode('utf-8','ignore'), 'html.parser') 9 | 10 | table = soup.findChild('table', {'class': 'halytyslista'}) 11 | 12 | alerts = [] 13 | active_alert = None 14 | 15 | class Alert: 16 | 17 | def __init__(self, kunta, aika, lyhyt_kuvaus): 18 | self.kunta = kunta 19 | self.aika = aika 20 | self.lyhyt_kuvaus = lyhyt_kuvaus 21 | self.kuvaus = '' 22 | 23 | def add_detail(self, kuvaus): 24 | self.kuvaus += kuvaus + '\n' 25 | 26 | for tr in table.find_all('tr'): 27 | 28 | if 'halytys' in tr.get('class', []): 29 | kunta = tr.findChild('td', {'class': 'kunta'}) 30 | if kunta.text.strip() in ('Espoo','Helsinki','Vantaa','Kirkkonummi'): 31 | aika = tr.findChild('td', {'class': 'pvm'}) 32 | lyhyt_kuvaus = aika.find_next('td').text 33 | active_alert = Alert(kunta.text, aika.text, lyhyt_kuvaus) 34 | alerts.append(active_alert) 35 | else: 36 | active_alert = None 37 | elif active_alert is not None: 38 | tarkemmat = tr.findChildren('td') 39 | for tarkempi_kuvaus in tarkemmat: 40 | for osa in tarkempi_kuvaus.strings: 41 | if osa != '': 42 | kuvaus = osa.replace('
','\n') 43 | active_alert.add_detail(kuvaus) 44 | for link in tarkempi_kuvaus.findChildren('a'): 45 | href = link['href'] 46 | if href.startswith('tehtava.php'): 47 | active_alert.add_detail('http://www.tilannehuone.fi/'+href) 48 | else: 49 | active_alert = None 50 | 51 | for alert in alerts[::-1]: 52 | print('-'*20) 53 | print(alert.kunta, alert.aika) 54 | print(alert.lyhyt_kuvaus) 55 | print() 56 | if alert.kuvaus != '': 57 | print(alert.kuvaus) 58 | print() 59 | -------------------------------------------------------------------------------- /custom-trainer.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | from ui import * 3 | from sound import * 4 | from speech import * 5 | from scripter import * 6 | 7 | moves = [ 8 | 'Skater', 9 | 'Pushups', 10 | 'Slalom jumps', 11 | 'Dips', 12 | 'Back', 13 | 'Jumping jacks' 14 | ] 15 | 16 | @script 17 | def exercise(v): 18 | for i in range(2): 19 | for move in moves: 20 | v.text = move 21 | say('Next up: ' + move +', prepare') 22 | breather(v) 23 | yield 24 | say('Now!') 25 | thirty_seconds(v) 26 | yield 2.0 27 | if i == 0: 28 | say('Two minute break') 29 | v.text = 'BREATHER' 30 | yield 90 31 | thirty_seconds(v) 32 | yield 33 | yield 2.0 34 | say_blocking(v, 'Ready') 35 | 36 | @script 37 | def thirty_seconds(v): 38 | #blip() 39 | yield 10 40 | blip() 41 | yield 10 42 | blip() 43 | yield 5 44 | for _ in range(5): 45 | blip() 46 | yield 1 47 | bleep() 48 | 49 | @script 50 | def breather(v): 51 | yield 10 52 | for _ in range(5): 53 | blip() 54 | yield 1 55 | 56 | def blip(): 57 | play_effect('piano:D3') 58 | 59 | def bleep(): 60 | play_effect('piano:D4') 61 | 62 | @script 63 | def say_blocking(v, text): 64 | say(text) 65 | while is_speaking(): yield 66 | 67 | if __name__ == '__main__': 68 | i = Button() 69 | #i.image = Image('iob:ios7_pause_256') 70 | i.tint_color = 'grey' 71 | i.background_color = 'white' 72 | v = Label() 73 | v.alignment = ALIGN_CENTER 74 | i.add_subview(v) 75 | v.frame = i.bounds 76 | v.flex = 'WH' 77 | v.touch_enabled = False 78 | i.present('sheet') 79 | 80 | v.height = i.height/2 81 | v.text = 'Tap to start' 82 | 83 | exercise(v) 84 | ctrl = find_scripter_instance(v) 85 | ctrl.pause_play_all() 86 | paused = True 87 | 88 | def play_pause(sender): 89 | global paused, ctrl 90 | ctrl.pause_play_all() 91 | if paused: 92 | i.image = Image('iob:ios7_play_256') 93 | paused = False 94 | else: 95 | i.image = Image('iob:ios7_pause_256') 96 | paused = True 97 | 98 | i.action = play_pause 99 | -------------------------------------------------------------------------------- /brython.py: -------------------------------------------------------------------------------- 1 | import console 2 | from objc_util import on_main_thread, NSURL 3 | import editor 4 | import os.path 5 | 6 | import wkwebview 7 | 8 | 9 | html_start = ''' 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 39 | ''' 40 | 41 | 42 | def main(): 43 | console.clear() 44 | 45 | class BrythonRunner(wkwebview.WKWebView): 46 | 47 | @on_main_thread 48 | def load_html(self, html): 49 | # Need to set a base directory to get 50 | # real js errors 51 | current_working_directory = os.path.dirname(editor.get_path()) 52 | root_dir = NSURL.fileURLWithPath_(current_working_directory) 53 | self.webview.loadHTMLString_baseURL_(html, root_dir) 54 | 55 | def _message(self, message): 56 | level, content = message['level'], message['content'] 57 | 58 | if level not in ['code', 'raw']: 59 | if content == 'using indexedDB for stdlib modules cache': 60 | #print('Brython started') 61 | return 62 | 63 | if level.upper() == 'LOG': 64 | print(content, 65 | end=('' if content.endswith('\n') else '\n')) 66 | return 67 | 68 | super()._message(message) 69 | 70 | python_code = editor.get_text() 71 | 72 | wv = BrythonRunner() 73 | 74 | html = html_start + python_code + html_end 75 | 76 | wv.load_html(html) 77 | 78 | if __name__ == '__main__': 79 | 80 | main() 81 | -------------------------------------------------------------------------------- /gym-log-07-Jul-2020: -------------------------------------------------------------------------------- 1 | 2019-08-05 Mikael Pohkeita 20 2 | 2019-08-05 Oskari Pohkeita 0 3 | 2019-08-05 Mikael Reisikääntö 50 4 | 2019-08-05 Oskari Reisikääntö 32 5 | 2019-08-05 Mikael Kyykky_penkille 20 6 | 2019-08-05 Oskari Kyykky_penkille 8 7 | 2019-08-05 Mikael Reisien_ojentajat 32 8 | 2019-08-05 Oskari Reisien_ojentajat 14 9 | 2019-08-10 Mikael Penkki-_punnerrus_painoilla 15 10 | 2019-08-10 Oskari Penkki-_punnerrus_painoilla 4 11 | 2019-08-10 Mikael Vinopenkki_painoilla 10 12 | 2019-08-10 Oskari Vinopenkki_painoilla 4 13 | 2019-08-11 Mikael Pohkeita 25 14 | 2019-08-11 Oskari Pohkeita 10 15 | 2019-08-11 Oskari Reisikääntö 32 16 | 2019-08-11 Mikael Reisikääntö 50 17 | 2019-08-11 Mikael Kyykky_penkille 20 18 | 2019-08-11 Oskari Kyykky_penkille 8 19 | 2019-08-11 Mikael Jalkaprässi 45 20 | 2019-08-11 Oskari Jalkaprässi 9 21 | 2019-08-11 Oskari Reisien_ojentajat 18 22 | 2019-08-11 Mikael Reisien_ojentajat 32 23 | 2019-08-11 Mikael Selkiä 1 24 | 2019-08-11 Oskari Selkiä 1 25 | 2019-08-14 Mikael Penkki-_punnerrus_painoilla 15 26 | 2019-08-14 Oskari Penkki-_punnerrus_painoilla 4 27 | 2019-08-15 Mikael Pohkeita 25 28 | 2019-08-15 Oskari Pohkeita 10 29 | 2019-08-15 Mikael Reisikääntö 64 30 | 2019-08-15 Mikael Reisikääntö 60 31 | 2019-08-15 Oskari Reisikääntö 32 32 | 2019-08-15 Oskari Reisien_ojentajat 27 33 | 2019-08-15 Mikael Reisien_ojentajat 36 34 | 2019-08-15 Oskari Jalkaprässi 18 35 | 2019-08-15 Mikael Jalkaprässi 64 36 | 2019-08-15 Oskari Kyykky_penkille 8 37 | 2019-08-15 Mikael Kyykky_penkille 20 38 | 2019-08-21 Mikael Pohkeita 25 39 | 2019-08-21 Oskari Pohkeita 10 40 | 2019-08-21 Oskari Reisikääntö 32 41 | 2019-08-21 Mikael Reisikääntö 60 42 | 2019-08-21 Mikael Kyykky_penkille 20 43 | 2019-08-21 Oskari Kyykky_penkille 8 44 | 2019-08-21 Mikael Jalkaprässi 73 45 | 2019-08-21 Oskari Jalkaprässi 27 46 | 2019-08-26 Mikael Pohkeita 64 47 | 2019-08-26 Oskari Pohkeita 26 48 | 2019-08-26 Mikael Reisikääntö 64 49 | 2019-08-26 Oskari Reisikääntö 34 50 | 2019-08-26 Mikael Jalkaprässi 82 51 | 2019-08-26 Oskari Jalkaprässi 36 52 | 2019-08-26 Mikael Reisien_ojentajat 50 53 | 2019-08-26 Oskari Reisien_ojentajat 32 54 | 2019-08-26 Mikael Selkiä 15 55 | 2019-08-26 Oskari Selkiä 5 56 | 2019-09-01 Mikael Pohkeita 100 57 | 2019-09-01 Oskari Pohkeita 36 58 | 2019-09-01 Mikael Reisikääntö 64+ 59 | 2019-09-01 Oskari Reisikääntö 36+ 60 | 2019-09-01 Mikael Jalkaprässi 82 61 | 2019-09-01 Oskari Jalkaprässi 36 62 | -------------------------------------------------------------------------------- /ruokaa.py: -------------------------------------------------------------------------------- 1 | import ui 2 | from tinysync import track 3 | 4 | 5 | font = 'Apple SD Gothic Neo' 6 | 7 | class FoodSource: 8 | 9 | def __init__(self, tableview): 10 | self.tableview = tableview 11 | self.ruoat = track([], 'ruoat') 12 | 13 | self.add_button = ui.ButtonItem( 14 | tint_color='black', 15 | title='Lisää', 16 | enabled=True, 17 | action=self.add, 18 | ) 19 | tableview.right_button_items = [self.add_button] 20 | 21 | self.last_created = None 22 | self.full_height = None 23 | 24 | def tableview_number_of_rows(self, tableview, section): 25 | return len(self.ruoat) 26 | 27 | def tableview_cell_for_row(self, tableview, section, row): 28 | cell = ui.TableViewCell() 29 | #cell.selectable = False 30 | cell.background_color='#f6f6f6' 31 | tf = ui.TextField( 32 | text=self.ruoat[row], 33 | row=row, 34 | placeholder='???', 35 | delegate=self, 36 | font=(font, 20), 37 | background_color='white', 38 | text_color='black', 39 | alignment=ui.ALIGN_LEFT, 40 | bordered=False, 41 | frame=cell.content_view.bounds.inset(8, 8, 8, 75), 42 | flex='WH', 43 | ) 44 | cell.content_view.add_subview(tf) 45 | self.last_created = tf 46 | return cell 47 | 48 | def tableview_delete(self, tableview, section, row): 49 | self.ruoat.remove(self.ruoat 50 | [row]) 51 | self.tableview.delete_rows([row]) 52 | 53 | def add(self, sender): 54 | self.ruoat.insert(0, '') 55 | self.tableview.insert_rows([0]) 56 | self.last_created.begin_editing() 57 | 58 | def tableview_can_delete(self, tableview, section, row): 59 | return True 60 | 61 | def tableview_did_select(self, tableview, section, row): 62 | moving = self.ruoat[row] 63 | self.tableview_delete(tableview, section, row) 64 | self.ruoat.append(moving) 65 | self.tableview.insert_rows([len(self.ruoat)-1]) 66 | 67 | def textfield_should_begin_editing(self, textfield): 68 | if self.full_height is None: 69 | self.full_height = self.tableview.height 70 | self.tableview.height = 434 71 | return True 72 | 73 | def textfield_did_end_editing(self, textfield): 74 | self.ruoat[textfield.row] = textfield.text 75 | self.tableview.height = self.full_height 76 | 77 | 78 | table = ui.TableView( 79 | background_color='white', 80 | ) 81 | 82 | source = FoodSource(table) 83 | table.data_source = source 84 | table.delegate = source 85 | 86 | 87 | table.present( 88 | 'fullscreen', 89 | animated=False, 90 | #hide_title_bar=True, 91 | #title_bar_color='black', 92 | orientations=['portrait'], 93 | ) 94 | 95 | -------------------------------------------------------------------------------- /bigletter/big-letter-service.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | colors = ( 4 | '#ccffcc', 5 | '#ccffff', 6 | '#ccccff', 7 | '#ffcccc', 8 | '#ffffcc', 9 | ) 10 | fonts = ( 11 | 'Arial', 12 | 'Arial Black', 13 | 'Verdana', 14 | 'Helvetica', 15 | 'Courier', 16 | ) 17 | 18 | def letter_handler(event, context=None): 19 | '@awslambda @html' 20 | 21 | try: 22 | letter = event['queryParams']['letter'] 23 | except KeyError: 24 | letter = 'Ö' 25 | 26 | color = random.choice(colors) 27 | font = random.choice(fonts) 28 | font = 'Arial Black' 29 | 30 | return f''' 31 | 32 | 33 | 34 | 35 | 36 | 44 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | ''' 77 | 78 | if __name__ == '__main__': 79 | import ui 80 | wv = ui.WebView() 81 | wv.present(hide_title_bar=True) 82 | html = letter_handler({}) 83 | wv.load_html(html) 84 | -------------------------------------------------------------------------------- /kalkylation.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal, ROUND_HALF_EVEN 2 | from functools import partial 3 | 4 | from sympy import solve, Eq 5 | from sympy.core import N 6 | from sympy.core.numbers import Number 7 | from sympy.core.sympify import SympifyError 8 | from sympy.parsing.sympy_parser import parse_expr 9 | from sympy.parsing.sympy_tokenize import generate_tokens 10 | 11 | 12 | class Kalkylation: 13 | """ 14 | >>> Kalkylation("1 2\\n3") 15 | Input: 1 + 2 + 3 16 | Symbolic result: 6 17 | Numeric result: 6.00 18 | 19 | >>> k = Kalkylation("1 = 2+x") 20 | >>> k 21 | Input: 1 = 2 + x 22 | Symbolic result: -1 23 | Numeric result: -1.00 24 | 25 | >>> k.tokens 26 | ['1', '=', '2', '+', 'x'] 27 | 28 | >>> Kalkylation.result_decimals = '.000001' 29 | 30 | >>> Kalkylation("1+23 =3+x**2") 31 | Input: 1 + 23 = 3 + x ** 2 32 | Symbolic result: -sqrt(21), sqrt(21) 33 | Numeric result: -4.582576, 4.582576 34 | 35 | """ 36 | 37 | result_decimals = '.01' 38 | 39 | def __init__(self, input_text): 40 | input_text = self._maybe_add_plusses(input_text) 41 | 42 | if '=' in input_text: 43 | left, right = input_text.split('=', 1) 44 | results = solve(Eq(parse_expr(left), parse_expr(right))) 45 | else: 46 | left = input_text 47 | right = [] 48 | results = parse_expr(left) 49 | 50 | self.tokens = self._tokenize(left) 51 | if left and right: 52 | self.tokens.append('=') 53 | self.tokens.extend(self._tokenize(right)) 54 | self.as_text = ' '.join(self.tokens) 55 | 56 | if not isinstance(results, list): 57 | results = [results] 58 | self.symbolic = results 59 | self.numeric = [ 60 | Decimal(str(N(result))).quantize( 61 | Decimal(self.result_decimals), 62 | rounding=ROUND_HALF_EVEN 63 | ) for result in results] 64 | 65 | def __repr__(self): 66 | return ( 67 | f"Input: {self.as_text}\n" 68 | f"Symbolic result: {', '.join(list(map(str, self.symbolic)))}\n" 69 | f"Numeric result: {', '.join(list(map(str, self.numeric)))}" 70 | ) 71 | 72 | @staticmethod 73 | def _maybe_add_plusses(as_text): 74 | """ 75 | >>> Kalkylation._maybe_add_plusses("1 2.0\\n3") 76 | '1+2.0+3' 77 | >>> Kalkylation._maybe_add_plusses("1 + 2\\n3") 78 | '1 + 2 3' 79 | """ 80 | parts = as_text.split() 81 | try: 82 | [Number(part) for part in parts] 83 | as_text = "+".join(parts) 84 | except SympifyError: 85 | as_text = ' '.join(parts) 86 | return as_text 87 | 88 | @staticmethod 89 | def _tokenize(as_text): 90 | """ 91 | >>> Kalkylation._tokenize("1+ 23 ") 92 | ['1', '+', '23'] 93 | """ 94 | tokens = [ 95 | token[1] for token 96 | in generate_tokens( 97 | partial(next, iter([as_text])) 98 | ) 99 | if token[1].strip() 100 | ] 101 | return tokens 102 | 103 | -------------------------------------------------------------------------------- /gym-test-prog.yaml: -------------------------------------------------------------------------------- 1 | - exercises: 2 | - image_name: '' 3 | reps: 3 x 10 4 | title: Penkkipunnerrus painoilla 5 | weight_mikael: 20 6 | weight_oskari: 5+ 7 | - image_name: '' 8 | reps: 3 x 10 9 | title: Vinopenkki painoilla 10 | weight_mikael: 12,5 11 | weight_oskari: 5 12 | - image_name: hartiat_painoilla.JPG 13 | reps: 3 x 10 14 | title: Hartiat painoilla 15 | weight_mikael: 7 16 | weight_oskari: 4 17 | - image_name: '' 18 | reps: 2 x 15 19 | title: Ojentajat ylös 20 | weight_mikael: 15 21 | weight_oskari: 5 22 | - image_name: '' 23 | reps: 3 x 10 24 | title: Dippi 25 | weight_mikael: 9 26 | weight_oskari: 36- 27 | title: '' 28 | - exercises: 29 | - image_name: '' 30 | reps: 3 x 10 31 | title: Talja eteen 32 | weight_mikael: 47+ 33 | weight_oskari: 26 34 | - image_name: '' 35 | reps: 3 x 10 36 | title: Talja niskaan 37 | weight_mikael: 40++ 38 | weight_oskari: 19++ 39 | - image_name: '' 40 | reps: 3 x 15 41 | title: Soutu taljalla 42 | weight_mikael: 40 43 | weight_oskari: 12+ 44 | - image_name: '' 45 | reps: 3 x 10 46 | title: Hauiskääntö käsipainoilla 47 | weight_mikael: 8+ 48 | weight_oskari: 3+ 49 | - image_name: '' 50 | reps: 2 x 15 51 | title: Ranskalaiset 52 | weight_mikael: 17,5+ 53 | weight_oskari: 7 54 | - image_name: '' 55 | reps: 10 56 | title: Painot sivuille ja eteen 57 | weight_mikael: 7 58 | weight_oskari: 3 59 | - image_name: '' 60 | reps: 100 61 | title: Nyrkkiin puristus 62 | weight_mikael: '' 63 | weight_oskari: '' 64 | title: Selkäpäivä 65 | - exercises: 66 | - image_name: pohkeita.JPG 67 | reps: 2 x 20 68 | title: Pohkeita 69 | weight_mikael: '100' 70 | weight_oskari: 36 71 | - image_name: reisikääntö.JPG 72 | reps: 3 x 15 73 | title: Reisikääntö 74 | weight_mikael: 68 75 | weight_oskari: 41? 76 | - image_name: jalkaprässi.JPG 77 | reps: 2 x 20 78 | title: Jalkaprässi 79 | weight_mikael: 82 80 | weight_oskari: 36 81 | - image_name: reisien_ojentajat.JPG 82 | reps: 2 x 15 83 | title: Reisien ojentajat 84 | weight_mikael: 50+ 85 | weight_oskari: 32 86 | - image_name: Selkiä.JPG 87 | reps: 1 x 20 88 | title: Pakaran ojentajat 89 | weight_mikael: 18.75 90 | weight_oskari: 6.25 91 | - image_name: selkiä.JPG 92 | reps: 1 x 20 93 | title: Selkiä 94 | weight_mikael: 15 95 | weight_oskari: 5 96 | title: Jalkapäivä 97 | - exercises: 98 | - image_name: '' 99 | reps: 1 x ∞ 100 | title: Vatsat koneella 101 | weight_mikael: 50 102 | weight_oskari: 36? 103 | - image_name: '' 104 | reps: 1 x 20 105 | title: Vatsat ristiin 106 | - image_name: '' 107 | reps: 1 x ∞ 108 | title: Vatsat pallolla 109 | title: Vatsa 110 | - exercises: 111 | - image_name: '' 112 | title: Selän ojennus 113 | - image_name: '' 114 | title: Kylkien ojennus 115 | title: Selkä 116 | - exercises: 117 | - image_name: kyykky_penkille.JPG 118 | reps: 3 x 10 119 | title: Kyykky penkille 120 | weight_mikael: 20 121 | weight_oskari: 8 122 | - image_name: bench_press.JPG 123 | reps: 3 x 8 124 | title: Penkkipunnerrus tangolla 125 | weight_mikael: 50 126 | weight_oskari: 10 127 | title: Arkisto 128 | -------------------------------------------------------------------------------- /threadbare.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Design goals: 3 | - No need to prime loops etc. 4 | - As effortless coexistence between sync and async code as possible 5 | - Same decorator for simple threads, multiple parallel threads with common completion, with results and without results 6 | - Timeouts for when things go wrong 7 | ''' 8 | 9 | import concurrent.futures as cf 10 | from functools import partial, wraps 11 | import threading, inspect, traceback 12 | 13 | def threadbare(func): 14 | @wraps(func) 15 | def wrapper(*args, **kwargs): 16 | thread = threading.current_thread() 17 | thread.collector = getattr(thread, 'collector', set()) 18 | executor = cf.ThreadPoolExecutor() 19 | if inspect.isgeneratorfunction(func): 20 | gen = func(*args, **kwargs) 21 | future = executor.submit(_gen_runner, gen) 22 | else: 23 | future = executor.submit(func, *args, **kwargs) 24 | thread.collector.add(future) 25 | executor.shutdown(wait=False) 26 | return future 27 | 28 | return wrapper 29 | 30 | def _gen_runner(gen): 31 | thread = threading.current_thread() 32 | thread.collector = getattr(thread, 'collector', set()) 33 | first_round = True 34 | prev_value = None 35 | try: 36 | while True: 37 | if first_round: 38 | value = next(gen) 39 | first_round = False 40 | else: 41 | value = gen.send(prev_value) 42 | for future in cf.as_completed(thread.collector): 43 | future.result() 44 | thread.collector.clear() 45 | if type(value) is cf.Future: 46 | prev_value = value.result() 47 | elif ( 48 | type(value) in (tuple, list, set) and 49 | all((type(elem) is cf.Future for elem in value)) 50 | ): 51 | prev_value = type(value)([future.result() for future in value]) 52 | else: 53 | prev_value = value 54 | except StopIteration as stop: 55 | for future in cf.as_completed(thread.collector): 56 | future.result() 57 | thread.collector.clear() 58 | return stop.value 59 | except Exception as e: 60 | traceback.print_exc() 61 | 62 | 63 | if __name__ == '__main__': 64 | 65 | import time, requests, bs4 66 | 67 | @threadbare 68 | def main(): 69 | login() 70 | weather = yield get_weather() 71 | print(f'Weather is {weather}') 72 | 73 | results = yield [fetch(url) 74 | for url in ( 75 | 'https://python.org', 76 | 'http://omz-software.com/pythonista/', 77 | 'https://pypi.org' 78 | )] 79 | print('Retrieved pages:', results) 80 | 81 | logout() 82 | 83 | @threadbare 84 | def login(): 85 | time.sleep(1) 86 | print('Logged in') 87 | raise Exception('Testing exception') 88 | 89 | @threadbare 90 | def get_weather(): 91 | time.sleep(0.5) 92 | return 'fine' 93 | 94 | @threadbare 95 | def fetch(url): 96 | text = requests.get(url).text 97 | soup = bs4.BeautifulSoup(text, 'html.parser') 98 | title = soup.find('title') 99 | return title.string 100 | 101 | @threadbare 102 | def logout(): 103 | print('Logged out') 104 | 105 | login() 106 | -------------------------------------------------------------------------------- /agenr.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Design goals: 3 | - No need to prime loops etc. 4 | - As effortless coexistence between sync and async code as possible 5 | - Same decorator for simple threads, multiple parallel threads with common completion, with results and without results 6 | - Timeouts for when things go wrong 7 | 8 | 9 | Test cf: https://stackoverflow.com/questions/25474204/checking-up-on-a-concurrent-futures-threadpoolexecutor/25474898#25474898 10 | ''' 11 | send 12 | import concurrent.futures as cf 13 | from functools import partial, wraps 14 | import threading, inspect, traceback 15 | 16 | def agenr(func): 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | thread = threading.current_thread() 20 | thread.collector = getattr(thread, 'collector', set()) 21 | executor = cf.ThreadPoolExecutor() 22 | if inspect.isgeneratorfunction(func): 23 | gen = func(*args, **kwargs) 24 | else: 25 | gen = _func_wrapper(func, *args, **kwargs) 26 | future = executor.submit(_gen_runner, gen) 27 | thread.collector.add(future) 28 | executor.shutdown(wait=False) 29 | return future 30 | 31 | return wrapper 32 | 33 | def _func_wrapper(func, *args, **kwargs): 34 | yield 35 | return func(*args, **kwargs) 36 | 37 | def _gen_runner(gen): 38 | thread = threading.current_thread() 39 | thread.collector = getattr(thread, 'collector', set()) 40 | first_round = True 41 | prev_value = None 42 | try: 43 | while True: 44 | if first_round: 45 | value = next(gen) 46 | first_round = False 47 | else: 48 | value = gen.send(prev_value) 49 | for future in cf.as_completed(thread.collector): 50 | future.result() 51 | thread.collector.clear() 52 | if type(value) is cf.Future: 53 | prev_value = value.result() 54 | elif ( 55 | type(value) in (tuple, list, set) and 56 | all((type(elem) is cf.Future for elem in value)) 57 | ): 58 | prev_value = type(value)([future.result() for future in value]) 59 | else: 60 | prev_value = value 61 | except StopIteration as stop: 62 | for future in cf.as_completed(thread.collector): 63 | future.result() 64 | thread.collector.clear() 65 | return stop.value 66 | except Exception as e: 67 | traceback.print_exc() 68 | raise 69 | 70 | 71 | if __name__ == '__main__': 72 | 73 | import time, requests, bs4 74 | 75 | @agenr 76 | def main(): 77 | login() 78 | weather = yield get_weather() 79 | print(f'Weather is {weather}') 80 | 81 | results = yield [fetch(url) 82 | for url in ( 83 | 'https://python.org', 84 | 'http://omz-software.com/pythonista/', 85 | 'https://pypi.org' 86 | )] 87 | print('Retrieved pages:', results) 88 | 89 | logout() 90 | 91 | @agenr 92 | def login(): 93 | time.sleep(1) 94 | print('Logged in') 95 | #raise Exception('Testing exception') 96 | 97 | @agenr 98 | def get_weather(): 99 | time.sleep(0.5) 100 | return 'fine' 101 | 102 | @agenr 103 | def fetch(url): 104 | text = requests.get(url).text 105 | soup = bs4.BeautifulSoup(text, 'html.parser') 106 | title = soup.find('title') 107 | return title.string 108 | 109 | @agenr 110 | def logout(): 111 | print('Logged out') 112 | 113 | main() 114 | -------------------------------------------------------------------------------- /styleproperties.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | class StyleProps(): 4 | 5 | def to_css_color(self, color): 6 | if type(color) is str: 7 | return color 8 | if type(color) == tuple and len(color) >= 3: 9 | alpha = color[3] if len(color) == 4 else 1.0 10 | if all((component >= 1.0) for component in color): 11 | color_rgb = [int(component*255) for component in color[:3]] 12 | color_rgb.append(color[3] if len(color) == 4 else 1.0) 13 | color = tuple(color_rgb) 14 | return f'rgba{str(color)}' 15 | 16 | @property 17 | def align_horizontal(self): 18 | return 'NOT IMPLEMENTED' 19 | 20 | @align_horizontal.setter 21 | def align_horizontal(self, value): 22 | self.js().set_style('textAlign', f'{value}') 23 | 24 | @property 25 | def align_vertical(self): 26 | return 'NOT IMPLEMENTED' 27 | 28 | @align_vertical.setter 29 | def align_vertical(self, value): 30 | 31 | self.js().set_style('display', 'table-cell') 32 | self.js().set_style('verticalAlign', f'{value}') 33 | self.js().xpath('text()').set_style('position', f'relative') 34 | self.js().xpath('text()').set_style('height', f'100%') 35 | 36 | @property 37 | def background_color(self): 38 | return self.js().style('backgroundColor') 39 | 40 | @background_color.setter 41 | def background_color(self, value): 42 | self.js().set_style('backgroundColor', self.to_css_color(value)) 43 | 44 | @property 45 | def background_image(self): 46 | return self.js().style('backgroundImage') 47 | 48 | @background_image.setter 49 | def background_image(self, value): 50 | self.js().set_style('backgroundImage', self.to_css_color(value)) 51 | 52 | @property 53 | def font(self): 54 | return self.js().style('font') 55 | 56 | @font.setter 57 | def font(self, value): 58 | self.js().set_style('font', value) 59 | 60 | @property 61 | def font_family(self): 62 | return self.js().style('fontFamily') 63 | 64 | @font_family.setter 65 | def font_family(self, value): 66 | self.js().set_style('fontFamily', value) 67 | 68 | @property 69 | def font_size(self): 70 | return self.js().style('fontSize').strip('px') 71 | 72 | @font_size.setter 73 | def font_size(self, value): 74 | self.js().set_style('fontSize', str(value)+'px') 75 | 76 | @property 77 | def font_bold(self): 78 | value = self.js().style('fontWeight') 79 | return value == 'bold' 80 | 81 | @font_bold.setter 82 | def font_bold(self, value): 83 | value = 'bold' if value else 'normal' 84 | self.js().set_style('fontWeight', value) 85 | 86 | @property 87 | def font_small_caps(self): 88 | "Set to True to display text in small caps. " 89 | value = self.js().style('fontVariant') 90 | return value == 'small-caps' 91 | 92 | @font_small_caps.setter 93 | def font_small_caps(self, value): 94 | value = 'small-caps' if value else 'normal' 95 | self.js().set_style('fontVariant', value) 96 | 97 | @property 98 | def visible(self): 99 | "Set to False to hide the view. The layout of other views will remain anchored to a hidden view." 100 | value = self.js().style('visibility') 101 | return value == 'visible' 102 | 103 | @visible.setter 104 | def visible(self, value): 105 | value = 'visible' if value else 'hidden' 106 | self.js().set_style('visibility', value) 107 | 108 | def shadow(self, *args, color=None, inset=False): 109 | color = color or self.tint_color 110 | if len(args) == 1 and args[0] == False: 111 | attr_js = 'none' 112 | else: 113 | attr_js = 'inset ' if inset else '' 114 | for dimension in args: 115 | attr_js += f'{dimension}px ' 116 | attr_js += self.to_css_color(color) 117 | self.js().set_style('boxShadow', attr_js) -------------------------------------------------------------------------------- /gym-program.yaml: -------------------------------------------------------------------------------- 1 | - exercises: 2 | - image_name: '' 3 | reps: 3 x 10 4 | title: Penkkipunnerrus tangolla 5 | weight_mikael: '60' 6 | weight_oskari: '25' 7 | - image_name: '' 8 | reps: 3 x 10 9 | title: Vinopenkki painoilla 10 | weight_mikael: '45' 11 | weight_oskari: 20+? 12 | - image_name: hartiat_painoilla.JPG 13 | reps: 3 x 10 14 | title: Hartiat painoilla 15 | weight_mikael: '15' 16 | weight_oskari: '8' 17 | - image_name: '' 18 | reps: 2 x 15 19 | title: Ranskalaiset 20 | weight_mikael: 24,5 21 | weight_oskari: '10' 22 | - image_name: '' 23 | reps: 3 x 10 24 | title: Dippi 25 | weight_mikael: '0' 26 | weight_oskari: '27' 27 | - image_name: '' 28 | reps: 1x20 29 | title: Vatsat/Mittarimato 30 | weight_mikael: '73' 31 | weight_oskari: '36' 32 | title: Ojentajat 33 | - exercises: 34 | - image_name: '' 35 | reps: '10' 36 | title: Leuanveto 37 | weight_mikael: 0 38 | weight_oskari: 0 39 | - image_name: '' 40 | reps: 3 x 10 41 | title: Talja eteen 42 | weight_mikael: '67' 43 | weight_oskari: 33++ 44 | - image_name: '' 45 | reps: 3 x 10 46 | title: Talja niskaan 47 | weight_mikael: 57+ 48 | weight_oskari: '33' 49 | - image_name: '' 50 | reps: 3 x 15 51 | title: Soutu taljalla 52 | weight_mikael: '47' 53 | weight_oskari: 19+ 54 | - image_name: '' 55 | reps: 3 x 10 56 | title: Hauiskääntö 57 | weight_mikael: 24,5 58 | weight_oskari: '11' 59 | - image_name: '' 60 | reps: 3x10 61 | title: Rannekääntö 62 | weight_mikael: 0 63 | weight_oskari: 0 64 | - image_name: '' 65 | reps: 1x20 66 | title: Nyrkkiin puristus 67 | weight_mikael: '' 68 | weight_oskari: '' 69 | - image_name: '' 70 | reps: 10 71 | title: Painot sivuille ja eteen 72 | weight_mikael: 12,5 73 | weight_oskari: '5' 74 | title: Koukistajat 75 | - exercises: 76 | - image_name: pohkeita.JPG 77 | reps: 2 x 20 78 | title: Pohkeita 79 | weight_mikael: '181' 80 | weight_oskari: 73+ 81 | - image_name: reisikääntö.JPG 82 | reps: 3 x 15 83 | title: Reisikääntö 84 | weight_mikael: 86+ 85 | weight_oskari: 59+? 86 | - image_name: jalkaprässi.JPG 87 | reps: 2 x 20 88 | title: Jalkaprässi 89 | weight_mikael: 163+ 90 | weight_oskari: '82' 91 | - image_name: reisien_ojentajat.JPG 92 | reps: 2 x 15 93 | title: Reisien ojentajat 94 | weight_mikael: 91+ 95 | weight_oskari: 64+ 96 | - image_name: Selkiä.JPG 97 | reps: 1 x 20 98 | title: Pakaran ojentajat 99 | weight_mikael: 23,75 100 | weight_oskari: 6.25 101 | - image_name: '' 102 | reps: 1 x 20 103 | title: Kyljet/Rotaatio 104 | weight_mikael: '15' 105 | weight_oskari: '2.5' 106 | title: Jalat 107 | - exercises: 108 | - image_name: '' 109 | reps: 1 x ∞ 110 | title: Vatsat koneella 111 | weight_mikael: '59' 112 | weight_oskari: 36? 113 | - image_name: '' 114 | reps: 1 x 20 115 | title: Vatsat ristiin 116 | - image_name: '' 117 | reps: 1 x ∞ 118 | title: Vatsat pallolla 119 | title: Vatsa 120 | - exercises: 121 | - image_name: '' 122 | title: Selän ojennus 123 | weight_mikael: '15' 124 | weight_oskari: + 125 | - image_name: '' 126 | reps: 2p x 20 127 | title: Kylkien ojennus 128 | weight_mikael: 23,75 129 | weight_oskari: 13,75 130 | - image_name: '' 131 | reps: 3x15 132 | title: Lapapunnerrus 133 | weight_mikael: '-' 134 | weight_oskari: '-' 135 | title: Selkä 136 | - exercises: 137 | - image_name: kyykky_penkille.JPG 138 | reps: 3 x 10 139 | title: Kyykky penkille 140 | weight_mikael: 20 141 | weight_oskari: 8 142 | - image_name: bench_press.JPG 143 | reps: 3 x 8 144 | title: Penkkipunnerrus tangolla 145 | weight_mikael: 50 146 | weight_oskari: 10 147 | title: Arkisto 148 | - exercises: [] 149 | title: Ojentajat koneella? 150 | - exercises: 151 | - image_name: '' 152 | reps: 3x10 153 | title: Lunge 154 | weight_mikael: 0 155 | weight_oskari: 0 156 | title: Jalat kotona 157 | -------------------------------------------------------------------------------- /vector.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | class Vector (list): 4 | ''' Simple 2D vector class to make vector operations more convenient. If 5 | performance is a concern, you are probably better off looking at numpy. 6 | 7 | Supports the following operations: 8 | 9 | * Initialization from two arguments, two keyword arguments (`x` and `y`), 10 | tuple, list, or another Vector. 11 | * Equality and unequality comparisons to other vectors. For floating point 12 | numbers, equality tolerance is 1e-10. 13 | * `abs`, `int` and `round` 14 | * Addition and in-place addition 15 | * Subtraction 16 | * Multiplication and division by a scalar 17 | * `len`, which is the same as `magnitude`, see below. 18 | 19 | Sample usage: 20 | 21 | v = Vector(x = 1, y = 2) 22 | v2 = Vector(3, 4) 23 | v += v2 24 | assert str(v) == '[4, 6]' 25 | assert v / 2.0 == Vector(2, 3) 26 | assert v * 0.1 == Vector(0.4, 0.6) 27 | assert v.distance_to(v2) == math.sqrt(1+4) 28 | 29 | v3 = Vector(Vector(1, 2) - Vector(2, 0)) # -1.0, 2.0 30 | v3.magnitude *= 2 31 | assert v3 == [-2, 4] 32 | 33 | v3.radians = math.pi # 180 degrees 34 | v3.magnitude = 2 35 | assert v3 == [-2, 0] 36 | v3.degrees = -90 37 | assert v3 == [0, -2] 38 | ''' 39 | 40 | abs_tol = 1e-10 41 | 42 | def __init__(self, *args, **kwargs): 43 | x = kwargs.pop('x', None) 44 | y = kwargs.pop('y', None) 45 | 46 | if x and y: 47 | self.append(x) 48 | self.append(y) 49 | elif len(args) == 2: 50 | self.append(args[0]) 51 | self.append(args[1]) 52 | else: 53 | super().__init__(*args, **kwargs) 54 | 55 | @property 56 | def x(self): 57 | ''' x component of the vector. ''' 58 | return self[0] 59 | 60 | @x.setter 61 | def x(self, value): 62 | self[0] = value 63 | 64 | @property 65 | def y(self): 66 | ''' y component of the vector. ''' 67 | return self[1] 68 | 69 | @y.setter 70 | def y(self, value): 71 | self[1] = value 72 | 73 | def __eq__(self, other): 74 | return math.isclose(self[0], other[0], abs_tol=self.abs_tol) and math.isclose(self[1], other[1], abs_tol=self.abs_tol) 75 | 76 | def __ne__(self, other): 77 | return not self.__eq__(other) 78 | 79 | def __abs__(self): 80 | return type(self)(abs(self.x), abs(self.y)) 81 | 82 | def __int__(self): 83 | return type(self)(int(self.x), int(self.y)) 84 | 85 | def __add__(self, other): 86 | return type(self)(self.x + other.x, self.y + other.y) 87 | 88 | def __iadd__(self, other): 89 | self.x += other.x 90 | self.y += other.y 91 | return self 92 | 93 | def __sub__(self, other): 94 | return type(self)(self.x - other.x, self.y - other.y) 95 | 96 | def __mul__(self, other): 97 | return type(self)(self.x * other, self.y * other) 98 | 99 | def __truediv__(self, other): 100 | return type(self)(self.x / other, self.y / other) 101 | 102 | def __len__(self): 103 | return self.magnitude 104 | 105 | def __round__(self): 106 | return type(self)(round(self.x), round(self.y)) 107 | 108 | def dot_product(self, other): 109 | ''' Sum of multiplying x and y components with the x and y components of another vector. ''' 110 | return self.x * other.x + self.y * other.y 111 | 112 | def distance_to(self, other): 113 | ''' Linear distance between this vector and another. ''' 114 | return (Vector(other) - self).magnitude 115 | 116 | @property 117 | def magnitude(self): 118 | ''' Length of the vector, or distance from (0,0) to (x,y). ''' 119 | return math.hypot(self.x, self.y) 120 | 121 | @magnitude.setter 122 | def magnitude(self, m): 123 | r = self.radians 124 | self.polar(r, m) 125 | 126 | @property 127 | def radians(self): 128 | ''' Angle between the positive x axis and this vector, in radians. ''' 129 | #return round(math.atan2(self.y, self.x), 10) 130 | return math.atan2(self.y, self.x) 131 | 132 | @radians.setter 133 | def radians(self, r): 134 | m = self.magnitude 135 | self.polar(r, m) 136 | 137 | def polar(self, r, m): 138 | ''' Set vector in polar coordinates. `r` is the angle in radians, `m` is vector magnitude or "length". ''' 139 | self.y = math.sin(r) * m 140 | self.x = math.cos(r) * m 141 | 142 | @property 143 | def degrees(self): 144 | ''' Angle between the positive x axis and this vector, in degrees. ''' 145 | return math.degrees(self.radians) 146 | 147 | @degrees.setter 148 | def degrees(self, d): 149 | self.radians = math.radians(d) 150 | 151 | def steps_to(self, other, step_magnitude=1.0): 152 | """ Generator that returns points on the line between this and the other point, with each step separated by `step_magnitude`. Does not include the starting point. """ 153 | if self == other: 154 | yield other 155 | else: 156 | step_vector = other - self 157 | steps = math.floor(step_vector.magnitude/step_magnitude) 158 | step_vector.magnitude = step_magnitude 159 | current_position = Vector(self) 160 | for _ in range(steps): 161 | current_position += step_vector 162 | yield Vector(current_position) 163 | if current_position != other: 164 | yield other 165 | 166 | def rounded_steps_to(self, other, step_magnitude=1.0): 167 | ''' As `steps_to`, but returns points rounded to the nearest integer. ''' 168 | for step in self.steps_to(other): 169 | yield round(step) 170 | -------------------------------------------------------------------------------- /objc_plus 2.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import types 3 | import uuid 4 | 5 | import objc_util 6 | 7 | 8 | class ObjCPlus: 9 | 10 | def __new__(cls, *args, **kwargs): 11 | objc_class = getattr(cls, '_objc_class', None) 12 | if objc_class is None: 13 | objc_class_name = cls.__name__ + '_ObjC' 14 | objc_superclass = getattr( 15 | cls, '_objc_superclass', objc_util.NSObject) 16 | objc_debug = getattr(cls, '_objc_debug', True) 17 | 18 | #'TempClass_'+str(uuid.uuid4())[-12:] 19 | 20 | objc_methods = [] 21 | objc_classmethods = [] 22 | for key in cls.__dict__: 23 | value = getattr(cls, key) 24 | if (inspect.isfunction(value) and 25 | '_self' in inspect.signature(value).parameters 26 | ): 27 | if getattr(value, '__self__', None) == cls: 28 | objc_classmethods.append(value) 29 | else: 30 | objc_methods.append(value) 31 | ''' 32 | objc_methods = [value 33 | for value in cls.__dict__.values() 34 | if ( 35 | callable(value) and 36 | '_self' in inspect.signature(value).parameters 37 | ) 38 | ] 39 | ''' 40 | if ObjCDelegate in cls.__mro__: 41 | objc_protocols = [cls.__name__] 42 | else: 43 | objc_protocols = getattr(cls, '_objc_protocols', []) 44 | if not type(objc_protocols) is list: 45 | objc_protocols = [objc_protocols] 46 | cls._objc_class = objc_class = objc_util.create_objc_class( 47 | objc_class_name, 48 | superclass=objc_superclass, 49 | methods=objc_methods, 50 | classmethods=objc_classmethods, 51 | protocols=objc_protocols, 52 | debug=objc_debug 53 | ) 54 | 55 | instance = objc_class.alloc().init() 56 | 57 | for key in dir(cls): 58 | value = getattr(cls, key) 59 | if inspect.isfunction(value): 60 | if not '_self' in inspect.signature(value).parameters: 61 | setattr(instance, key, types.MethodType(value, instance)) 62 | if key == '__init__': 63 | value(instance, *args, **kwargs) 64 | return instance 65 | 66 | 67 | class ObjCDelegate(ObjCPlus): 68 | """ If you inherit from this class, the class name must match the delegate 69 | protocol name. """ 70 | 71 | 72 | 73 | if __name__ == '__main__': 74 | 75 | class TestClass(ObjCPlus): 76 | 77 | def __init__(self): 78 | self.test_variable = 'Instance attribute' 79 | 80 | instance = TestClass() 81 | assert instance.test_variable == 'Instance attribute' 82 | assert type(instance) is objc_util.ObjCInstance 83 | 84 | class GestureHandler(ObjCPlus): 85 | 86 | # Can be a single string or a list 87 | _objc_protocols = 'UIGestureRecognizerDelegate' 88 | 89 | # Vanilla Python __init__ 90 | def __init__(self): 91 | self.other_recognizers = [] 92 | 93 | # ObjC delegate method 94 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_( 95 | _self, _sel, _gr, _other_gr): 96 | self = ObjCInstance(_self) 97 | other_gr = ObjCInstance(_other_gr) 98 | return other_gr in self.other_recognizers 99 | 100 | # Custom ObjC action target 101 | def gestureAction(_self, _cmd): 102 | self = ObjCInstance(_self) 103 | ... 104 | 105 | # Custom ObjC class method 106 | @classmethod 107 | def gestureType(_class, _cmd): 108 | ... 109 | 110 | # Vanilla Python method 111 | @objc_util.on_main_thread 112 | def before(self): 113 | return self.other_recognizers 114 | 115 | handler = GestureHandler() 116 | assert type(handler) is objc_util.ObjCInstance 117 | assert type(handler.other_recognizers) is list 118 | assert type(handler.before()) is list 119 | assert hasattr(handler, 'gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_') 120 | print(handler.__dict__) 121 | 122 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_( 123 | _self, _sel, _gr, _other_gr): 124 | self = ObjCInstance(_self) 125 | other_gr = ObjCInstance(_other_gr) 126 | return other_gr in self.other_recognizers 127 | 128 | # Custom ObjC action target 129 | def gestureAction(_self, _cmd): 130 | self = ObjCInstance(_self) 131 | ... 132 | 133 | GestureHandlerObjC = objc_util.create_objc_class( 134 | 'GestureHandlerObjC', 135 | methods=[ 136 | gestureAction, 137 | gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_, 138 | ], 139 | protocols=['UIGestureRecognizerDelegate'], 140 | ) 141 | 142 | class GestureHandler2(ObjCPlus): 143 | 144 | _objc_class = GestureHandlerObjC 145 | 146 | # Vanilla Python __init__ 147 | def __init__(self): 148 | self.other_recognizers = [] 149 | 150 | # Vanilla Python method 151 | @objc_util.on_main_thread 152 | def before(self): 153 | return self.other_recognizers 154 | 155 | handler = GestureHandler2() 156 | 157 | assert type(handler) is objc_util.ObjCInstance 158 | assert type(handler.other_recognizers) is list 159 | assert type(handler.before()) is list 160 | 161 | -------------------------------------------------------------------------------- /objc_plus.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import types 3 | import uuid 4 | 5 | import objc_util 6 | 7 | 8 | class ObjCPlus: 9 | 10 | def __new__(cls, *args, **kwargs): 11 | objc_class = getattr(cls, '_objc_class', None) 12 | if objc_class is None: 13 | objc_class_name = cls.__name__ + '_ObjC' 14 | objc_superclass = getattr( 15 | cls, '_objc_superclass', objc_util.NSObject) 16 | objc_debug = getattr(cls, '_objc_debug', True) 17 | 18 | #'TempClass_'+str(uuid.uuid4())[-12:] 19 | 20 | objc_methods = [] 21 | objc_classmethods = [] 22 | for key in cls.__dict__: 23 | value = getattr(cls, key) 24 | if (inspect.isfunction(value) and 25 | '_self' in inspect.signature(value).parameters 26 | ): 27 | if getattr(value, '__self__', None) == cls: 28 | objc_classmethods.append(value) 29 | else: 30 | objc_methods.append(value) 31 | ''' 32 | objc_methods = [value 33 | for value in cls.__dict__.values() 34 | if ( 35 | callable(value) and 36 | '_self' in inspect.signature(value).parameters 37 | ) 38 | ] 39 | ''' 40 | if ObjCDelegate in cls.__mro__: 41 | objc_protocols = [cls.__name__] 42 | else: 43 | objc_protocols = getattr(cls, '_objc_protocols', []) 44 | if not type(objc_protocols) is list: 45 | objc_protocols = [objc_protocols] 46 | cls._objc_class = objc_class = objc_util.create_objc_class( 47 | objc_class_name, 48 | superclass=objc_superclass, 49 | methods=objc_methods, 50 | classmethods=objc_classmethods, 51 | protocols=objc_protocols, 52 | debug=objc_debug 53 | ) 54 | 55 | instance = objc_class.alloc().init() 56 | 57 | for key in dir(cls): 58 | value = getattr(cls, key) 59 | if inspect.isfunction(value): 60 | if not '_self' in inspect.signature(value).parameters: 61 | setattr(instance, key, types.MethodType(value, instance)) 62 | if key == '__init__': 63 | value(instance, *args, **kwargs) 64 | return instance 65 | 66 | 67 | class ObjCDelegate(ObjCPlus): 68 | """ If you inherit from this class, the class name must match the delegate 69 | protocol name. """ 70 | 71 | 72 | 73 | if __name__ == '__main__': 74 | 75 | class TestClass(ObjCPlus): 76 | 77 | def __init__(self): 78 | self.test_variable = 'Instance attribute' 79 | 80 | instance = TestClass() 81 | assert instance.test_variable == 'Instance attribute' 82 | assert type(instance) is objc_util.ObjCInstance 83 | 84 | class GestureHandler(ObjCPlus): 85 | 86 | # Can be a single string or a list 87 | _objc_protocols = 'UIGestureRecognizerDelegate' 88 | 89 | # Vanilla Python __init__ 90 | def __init__(self): 91 | self.other_recognizers = [] 92 | 93 | # ObjC delegate method 94 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_( 95 | _self, _sel, _gr, _other_gr): 96 | self = ObjCInstance(_self) 97 | other_gr = ObjCInstance(_other_gr) 98 | return other_gr in self.other_recognizers 99 | 100 | # Custom ObjC action target 101 | def gestureAction(_self, _cmd): 102 | self = ObjCInstance(_self) 103 | ... 104 | 105 | # Custom ObjC class method 106 | @classmethod 107 | def gestureType(_class, _cmd): 108 | ... 109 | 110 | # Vanilla Python method 111 | @objc_util.on_main_thread 112 | def before(self): 113 | return self.other_recognizers 114 | 115 | handler = GestureHandler() 116 | assert type(handler) is objc_util.ObjCInstance 117 | assert type(handler.other_recognizers) is list 118 | assert type(handler.before()) is list 119 | assert hasattr(handler, 'gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_') 120 | 121 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_( 122 | _self, _sel, _gr, _other_gr): 123 | self = ObjCInstance(_self) 124 | other_gr = ObjCInstance(_other_gr) 125 | return other_gr in self.other_recognizers 126 | 127 | # Custom ObjC action target 128 | def gestureAction(_self, _cmd): 129 | self = ObjCInstance(_self) 130 | ... 131 | 132 | GestureHandlerObjC = objc_util.create_objc_class( 133 | 'GestureHandlerObjC', 134 | methods=[ 135 | gestureAction, 136 | gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_, 137 | ], 138 | protocols=['UIGestureRecognizerDelegate'], 139 | ) 140 | 141 | class GestureHandler2(ObjCPlus): 142 | 143 | _objc_class = GestureHandlerObjC 144 | 145 | # Vanilla Python __init__ 146 | def __init__(self): 147 | self.other_recognizers = [] 148 | 149 | # Vanilla Python method 150 | @objc_util.on_main_thread 151 | def before(self): 152 | return self.other_recognizers 153 | 154 | handler = GestureHandler2() 155 | 156 | assert type(handler) is objc_util.ObjCInstance 157 | assert type(handler.other_recognizers) is list 158 | assert type(handler.before()) is list 159 | 160 | 161 | class UIGestureRecognizerDelegate(ObjCDelegate): 162 | 163 | # Vanilla Python __init__ 164 | def __init__(self): 165 | self.other_recognizers = [] 166 | 167 | # ObjC delegate method 168 | def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_( 169 | _self, _sel, _gr, _other_gr): 170 | self = ObjCInstance(_self) 171 | other_gr = ObjCInstance(_other_gr) 172 | return other_gr in self.other_recognizers 173 | 174 | # Custom ObjC action target 175 | def gestureAction(_self, _cmd): 176 | self = ObjCInstance(_self) 177 | ... 178 | 179 | # Vanilla Python method 180 | @objc_util.on_main_thread 181 | def before(self): 182 | return self.other_recognizers 183 | 184 | handler = UIGestureRecognizerDelegate() 185 | assert type(handler) is objc_util.ObjCInstance 186 | assert type(handler.other_recognizers) is list 187 | assert type(handler.before()) is list 188 | assert hasattr(handler, 'gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_') 189 | 190 | -------------------------------------------------------------------------------- /gym-list.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | import ui 4 | import anchor 5 | import pygestures 6 | 7 | import tinysync 8 | 9 | import functools, types, datetime 10 | 11 | 12 | dark_background = '#0e0e0e' 13 | default_tint = '#fffade' # '#e6fbff' 14 | default_highlight = '#7faa6e' 15 | font = 'Apple SD Gothic Neo' 16 | 17 | class DaySource: 18 | 19 | def __init__(self, tableview, exercises): 20 | self.ex = exercises 21 | self.tableview = tableview 22 | self.full_height = None 23 | tableview.row_height = 100 24 | 25 | self.edit_button = ui.ButtonItem( 26 | tint_color='grey', 27 | title='Edit', 28 | enabled=True, 29 | action=self.edit, 30 | ) 31 | 32 | self.add_button = ui.ButtonItem( 33 | tint_color='grey', 34 | title='Add', 35 | enabled=True, 36 | action=self.add, 37 | ) 38 | 39 | tableview.right_button_items = [self.add_button, self.edit_button] 40 | 41 | def tableview_number_of_rows(self, tableview, section): 42 | return len(self.ex) 43 | 44 | def tableview_cell_for_row(self, tableview, section, row): 45 | cell = ui.TableViewCell() 46 | cell.selectable = False 47 | cell.background_color='black' 48 | tf = ui.TextField(text=self.ex[row].title, 49 | key='title', 50 | row=row, 51 | delegate=self, 52 | font = (font, 32), 53 | background_color='black', 54 | text_color='white', 55 | alignment=ui.ALIGN_CENTER, 56 | bordered=False, 57 | frame=cell.content_view.bounds.inset(8,8), 58 | flex='WH') 59 | cell.content_view.add_subview(tf) 60 | return cell 61 | 62 | def tableview_did_select(self, tableview, section, row): 63 | day = self.ex[row] 64 | 65 | ex_table = ui.TableView( 66 | background_color='black', 67 | frame=self.nav_view.bounds, flex='WH' 68 | ) 69 | 70 | ex_source = ExerciseSource(ex_table, day.exercises) 71 | 72 | ex_table.data_source = ex_source 73 | ex_table.delegate = ex_source 74 | 75 | self.nav_view.push_view(ex_table) 76 | 77 | def textfield_should_begin_editing(self, textfield): 78 | if self.tableview.editing: 79 | if self.full_height is None: 80 | self.full_height = self.tableview.height 81 | self.tableview.height = 450 82 | return True 83 | else: 84 | self.tableview_did_select(None, 0, textfield.row) 85 | return False 86 | 87 | def textfield_did_end_editing(self, textfield): 88 | self.ex[textfield.row][textfield.key] = textfield.text 89 | self.tableview.height = self.full_height 90 | #self.edit(self.edit_button) 91 | 92 | def tableview_move_row(self, tableview, from_section, from_row, to_section, to_row): 93 | self.ex.insert(to_row, self.ex.pop(from_row)) 94 | 95 | def tableview_delete(self, tableview, section, row): 96 | self.ex.remove(self.ex[row]) 97 | self.tableview.reload() 98 | 99 | def edit(self, sender): 100 | if not self.tableview.editing: 101 | self.tableview.set_editing(True, True) 102 | sender.title = 'Done' 103 | else: 104 | self.tableview.set_editing(False, True) 105 | sender.title = 'Edit' 106 | 107 | def add(self, sender): 108 | self.ex.append({ 109 | 'title': '?', 110 | 'exercises': [] 111 | }) 112 | self.tableview.reload() 113 | 114 | def tableview_can_delete(self, tableview, section, row): return True 115 | 116 | def tableview_can_move(self, tableview, section, row): return True 117 | 118 | 119 | class ExerciseSource(DaySource): 120 | 121 | def create_textfield(self, row, key, size, container): 122 | tf = anchor.TextField( 123 | text=str(self.ex[row].get(key, '')), 124 | row=row, 125 | key=key, 126 | delegate=self, 127 | font = (font, size), 128 | background_color='black', 129 | text_color='white', 130 | alignment=ui.ALIGN_CENTER, 131 | bordered=False, 132 | ) 133 | container.add_subview(tf) 134 | return tf 135 | 136 | def create_label(self, text, size, container): 137 | l = anchor.Label( 138 | text=text, 139 | font = (font, size), 140 | background_color='black', 141 | text_color='white', 142 | alignment=ui.ALIGN_RIGHT, 143 | ) 144 | container.add_subview(l) 145 | return l 146 | 147 | def tableview_cell_for_row(self, tableview, section, row): 148 | cell = ui.TableViewCell() 149 | cell.selectable = False 150 | cell.background_color = 'black' 151 | ex = self.ex[row] 152 | container = anchor.View( 153 | frame=cell.content_view.bounds, 154 | flex='WH') 155 | cell.content_view.add_subview(container) 156 | 157 | tf_title = self.create_textfield( 158 | row, 'title', 24, container) 159 | tf_title.dock.top_leading() 160 | tf_title.at.height == 40 161 | 162 | tf_reps = self.create_textfield( 163 | row, 'reps', 24, container) 164 | tf_reps.dock.top_trailing() 165 | tf_reps.at.width == 75 166 | tf_reps.align.height(tf_title) 167 | 168 | tf_title.at.trailing == tf_reps.at.leading_padding 169 | 170 | l_o = self.create_label('OSKARI', 10, container) 171 | l_m = self.create_label('MIKAEL', 10, container) 172 | l_filler = self.create_label('', 10, container) 173 | 174 | tf_w_o = self.create_textfield( 175 | row, 'weight_oskari', 24, container) 176 | tf_w_m = self.create_textfield( 177 | row, 'weight_mikael', 24, container) 178 | 179 | l_o.dock.bottom_leading() 180 | l_o.at.top == tf_title.at.bottom_padding 181 | l_o.align.top(l_m, l_filler, tf_w_o, tf_w_m) 182 | l_o.align.bottom(l_m, l_filler, tf_w_o, tf_w_m) 183 | l_o.align.width(l_m) 184 | l_filler.at.width == l_o.width/2 185 | tf_w_o.at.width == 75 186 | tf_w_o.align.width(tf_w_m) 187 | l_filler.at.trailing == container.at.trailing_margin 188 | tf_w_o.at.leading == l_o.at.trailing_padding 189 | l_m.at.leading == tf_w_o.at.trailing_padding 190 | tf_w_m.at.leading == l_m.at.trailing_padding 191 | l_filler.at.leading == tf_w_m.at.trailing_padding 192 | 193 | return cell 194 | 195 | def textfield_should_begin_editing(self, textfield): 196 | if self.full_height is None: 197 | self.full_height = self.tableview.height 198 | self.tableview.height = 433 199 | return True 200 | 201 | def add(self, sender): 202 | self.ex.append({ 203 | 'title': '?', 204 | 'image_name': '', 205 | 'reps': '3x10', 206 | 'weight_mikael': 0, 207 | 'weight_oskari': 0, 208 | }) 209 | self.tableview.reload() 210 | 211 | 212 | if __name__ == '__main__': 213 | 214 | exercises = tinysync.track([], name='gym-program') 215 | 216 | v = ui.View(background_color='black',) 217 | 218 | 219 | day_table = ui.TableView( 220 | background_color='black', 221 | frame=v.bounds, flex='WH', 222 | ) 223 | 224 | day_source = DaySource(day_table, exercises) 225 | day_table.data_source = day_source 226 | day_table.delegate = day_source 227 | 228 | n = anchor.NavigationView( 229 | day_table, 230 | navigation_bar_hidden=False, 231 | background_color='black', 232 | title_color='black', 233 | tint_color='grey', 234 | bar_tint_color='black', 235 | frame=v.bounds, flex='WH') 236 | day_source.nav_view = n 237 | #n.objc_instance.navigationController().navigationBar().hidden = True 238 | 239 | v.add_subview(n) 240 | n.dock.all(fit=anchor.Dock.SAFE) 241 | 242 | v.present( 243 | 'fullscreen', 244 | animated=False, 245 | hide_title_bar=True, 246 | title_bar_color='black', 247 | orientations=['portrait']) 248 | -------------------------------------------------------------------------------- /wikipedia-browser.py: -------------------------------------------------------------------------------- 1 | import wikipedia 2 | from unsync import unsync 3 | import ui 4 | from scene import Rect, Size 5 | from anchor import * 6 | from vector import Vector 7 | import time 8 | from random import * 9 | import spritekit as sk 10 | from objc_util import on_main_thread, ObjCInstance 11 | 12 | 13 | class WikipediaGraph: 14 | 15 | def __init__(self, scene): 16 | self.scene = scene 17 | self.selected_page = None 18 | self.loaded_content = None 19 | self.nodes = {} 20 | self.connectors = [] 21 | 22 | @unsync 23 | def search_term(self, term): 24 | for node in self.nodes.values(): 25 | node.parent = None 26 | hits = wikipedia.search(term) 27 | for hit in hits: 28 | if term.lower() == hit.lower(): 29 | self.put_on_screen([hit]) 30 | break 31 | else: 32 | self.put_on_screen(hits[:20]) 33 | 34 | @on_main_thread 35 | def put_on_screen(self, hits, parent=None): 36 | for hit in hits: 37 | pos_v = Vector(randint(100,150),0) 38 | pos_v.degrees = randint(1, 360) 39 | self.nodes[hit] = node = GraphNode(hit, 40 | position=tuple(pos_v), 41 | charge=1.0, 42 | mass=1.0, 43 | field_bitmask=3, 44 | parent=self.scene 45 | ) 46 | if parent is not None: 47 | j = sk.Joint.spring(parent, node, 48 | parent.position, node.position, 49 | frequency=0.8, damping=0.2) 50 | #print(dir(ObjCInstance(j))) 51 | self.connectors.append((parent, node)) 52 | if len(hits) == 1: 53 | self.fetch_page(hits[0]) 54 | 55 | @on_main_thread 56 | def no_match(self): 57 | self.nodes = { 'Not found': 58 | sk.LabelNode('Not found', 59 | position=(0,100), 60 | font_color='grey', 61 | alignment=sk.LabelNode.ALIGN_CENTER, 62 | parent=self.scene 63 | ) 64 | } 65 | 66 | @unsync 67 | def fetch_page(self, name): 68 | if name != self.selected_page: 69 | self.selected_page = name 70 | node = self.nodes[name] 71 | node.dynamic = False 72 | node.run_action( 73 | sk.Action.move_to((0,0))) 74 | for key in self.nodes: 75 | if key != name: 76 | self.nodes[key].parent = None 77 | self.nodes = { name: node } 78 | self.loaded_content = wikipedia.page(name) 79 | if self.loaded_content.title == name: 80 | self.put_on_screen(self.loaded_content.links[:20], node) 81 | else: 82 | self.no_match() 83 | 84 | 85 | class SpringScene(sk.Scene): 86 | 87 | def __init__(self, **kwargs): 88 | super().__init__(**kwargs) 89 | 90 | self.scene.connectors = sk.ShapeNode( 91 | ui.Path(), 92 | no_body=True, 93 | line_color='grey', 94 | parent=self.scene, 95 | ) 96 | 97 | sk.FieldNode.electric( 98 | strength=0.2, 99 | falloff=1.5, 100 | minimum_radius=10, 101 | category_bitmask=1, 102 | parent=self.scene 103 | ) 104 | 105 | sk.FieldNode.radial_gravity( 106 | strength=0.2, 107 | falloff=1, 108 | minimum_radius=10, 109 | parent=self.scene 110 | ) 111 | 112 | def layout(self): 113 | x,y,w,h = self.view.bounds 114 | self.set_edge_loop( 115 | *self.convert_from_view((0,h)), 116 | w,h 117 | ) 118 | 119 | def update(self, ct): 120 | return 121 | if not hasattr(self, 'graph'): return 122 | if len(self.graph.connectors) == 0: 123 | return 124 | p = ui.Path() 125 | for a,b in self.graph.connectors: 126 | a_pos = self.convert_point_to( 127 | a.position, self.connectors) 128 | b_pos = self.convert_point_to( 129 | b.position, self.connectors) 130 | p.move_to(*a_pos) 131 | p.line_to(*b_pos) 132 | self.connectors.path = p 133 | 134 | class GraphNode(sk.BoxNode): 135 | 136 | def __init__(self, text, 137 | fill_color='lightgrey', 138 | font_color='black', 139 | font=('Apple SD Gothic Neo', 12), 140 | **kwargs): 141 | 142 | self.text = text 143 | 144 | size = ui.measure_string(text, 145 | max_width=150, 146 | font=font, 147 | alignment=ui.ALIGN_CENTER, 148 | ) 149 | outer = size + Size(32, 16) 150 | inner = size + Size(24, 8) 151 | super().__init__(outer, 152 | fill_color='transparent', 153 | line_color='transparent', 154 | **kwargs) 155 | self.visible_background = sk.BoxNode(inner, 156 | fill_color=fill_color, 157 | no_body=True, 158 | parent=self 159 | ) 160 | label = sk.LabelNode(text, 161 | font=font, 162 | font_color=font_color, 163 | max_width=150, 164 | line_break_mode=ui.LB_WORD_WRAP, 165 | number_of_lines=0, 166 | alignment=sk.LabelNode.ALIGN_CENTER, 167 | vertical_alignment=sk.LabelNode.ALIGN_MIDDLE, 168 | parent=self.visible_background 169 | ) 170 | self.touch_enabled = True 171 | 172 | def touch_began(self, t): 173 | self.start_time = time.time() 174 | self.prev_pos = self.convert_point_to(t.location, self.scene) 175 | self.dynamic = False 176 | 177 | def touch_moved(self, t): 178 | scene_pos = self.convert_point_to(t.location, self.scene) 179 | self.position += (scene_pos - self.prev_pos) 180 | self.prev_pos = scene_pos 181 | 182 | def touch_ended(self, t): 183 | self.dynamic = True 184 | if time.time() - self.start_time < 0.3: 185 | self.visible_background.fill_color = 'red' 186 | self.parent.graph.fetch_page(self.text) 187 | 188 | 189 | class SectionNode(GraphNode): 190 | pass 191 | 192 | class WikipediaBrowser(ui.View): 193 | 194 | def __init__(self, **kwargs): 195 | super().__init__(**kwargs) 196 | self.previous_size_class = None 197 | self.active_constraints = [] 198 | enable(self) 199 | self.create_ui() 200 | 201 | def style(self, view): 202 | #view.background_color='black' 203 | #view.border_color = 'black' 204 | #view.border_width = 1 205 | #view.text_color = 'black' 206 | view.tint_color = 'black' 207 | 208 | def create_ui(self): 209 | self.style(self) 210 | 211 | main_frame = View(name='Main frame') 212 | self.add_subview(main_frame) 213 | main_frame.dock.all(fit=Dock.SAFE) 214 | 215 | self.search_field = search_field = TextField( 216 | name='Searchfield', 217 | placeholder='Search term', 218 | clear_button_mode='always', 219 | action=self.search, 220 | ) 221 | main_frame.add_subview(search_field) 222 | self.style(search_field) 223 | 224 | search_button = Button( 225 | name='Search', 226 | title='Search', 227 | action=self.search, 228 | ).dock.fit() 229 | main_frame.add_subview(search_button) 230 | self.style(search_button) 231 | 232 | self.scene = SpringScene( 233 | physics=sk.UIPhysics, 234 | #physics_debug=True, 235 | background_color='white', 236 | anchor_point=(0.5, 0.5) 237 | ) 238 | 239 | result_area = self.scene.view 240 | enable(result_area) 241 | main_frame.add_subview(result_area) 242 | 243 | search_field.dock.top_leading() 244 | search_button.dock.top_trailing() 245 | search_field.at.trailing == search_button.at.leading_padding 246 | search_field.align.height(search_button) 247 | 248 | result_area.dock.bottom() 249 | result_area.at.top == search_field.at.bottom_padding 250 | 251 | self.scene.graph = WikipediaGraph(self.scene) 252 | self.search_field.begin_editing() 253 | 254 | def search(self, sender): 255 | self.search_field.end_editing() 256 | search_term = self.search_field.text 257 | self.scene.graph.search_term(search_term) 258 | 259 | 260 | root = WikipediaBrowser() 261 | root.present('full_screen', hide_title_bar=True, animated=False) 262 | -------------------------------------------------------------------------------- /helmet-list.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import requests, bs4, reminders, ui, sound, console, random, json, time, clipboard 3 | from urllib import parse 4 | from objc_util import * 5 | from ctypes import c_void_p 6 | import inheritable 7 | from jswrapper import * 8 | 9 | from xpath import dsl as xp 10 | from xpath.renderer import to_xpath 11 | 12 | first = False 13 | 14 | #logins = ( 15 | # ('Name', 'Card_number', 'PIN'), ('...','...','...')) 16 | from helmetids import logins as logins 17 | 18 | class HelmetScraper(WebScraper): 19 | 20 | list_name = 'Kirjaston kirjat' 21 | 22 | def __init__(self, **kwargs): 23 | super().__init__(**kwargs) 24 | self.url_map = { 25 | self.default: 'about:blank', 26 | self.login_page: 'https://luettelo.helmet.fi/iii/cas/login', 27 | self.loans_page: 'https://haku.helmet.fi/iii/mobile/myaccount', 28 | self.renew_confirmation: 'https://haku.helmet.fi/iii/mobile/myaccount', 29 | self.capture_list: 'https://haku.helmet.fi/iii/mobile/myaccount', 30 | self.logged_out: 'https://haku.helmet.fi/iii/mobile/homepage' 31 | } 32 | 33 | def default(self): 34 | global first 35 | if first: 36 | action = 1 37 | first = False 38 | else: 39 | try: 40 | action = console.alert('Helmet', button1='Uusi lainat', button2='Skannaa kassiin', button3='Hae lainat') 41 | except KeyboardInterrupt: 42 | self.close() 43 | return 44 | 45 | if action == 1 or action == 3: 46 | db = self.get_reminder_list() 47 | self.checked_titles = [reminder.title for reminder in reminders.get_reminders(db, completed=True)] 48 | reminders.delete_calendar(db) 49 | self.db = self.create_reminder_list() 50 | 51 | self.handler = self.login_page 52 | self.action_type = 'renewing' if action == 1 else 'retrieving' 53 | self.person_index = 0 54 | self.start_per_person() 55 | 56 | if action == 2: 57 | start_scanning() 58 | self.default() 59 | 60 | def start_per_person(self): 61 | print('Person:', logins[self.person_index][0]) 62 | self.handler = self.login_page 63 | self.load_url('https://haku.helmet.fi/iii/mobile/myaccount?lang=fin&suite=mobile') 64 | 65 | def login_page(self): 66 | self.set_field('code', logins[self.person_index][1]) 67 | self.set_field('pin', logins[self.person_index][2]) 68 | self.handler = self.loans_page 69 | self.by_id('fm1').submit() 70 | 71 | def loans_page(self): 72 | 73 | loans_iframe = self.by_id('accountContentIframe').frame_body() 74 | content = loans_iframe.html() 75 | #print(content) 76 | 77 | if 'patFuncNoEntries' in content: 78 | print('Ei lainoja') 79 | self.logout() 80 | else: 81 | print(loans_iframe.value('th[@class="patFuncTitle"]').strip()) 82 | if self.action_type == 'renewing': 83 | self.handler = self.renew_confirmation 84 | loans_iframe.frame_window().dot('submitCheckout("requestRenewAll", "requestRenewAll");').evaluate() 85 | else: 86 | self.capture_list() 87 | 88 | def renew_confirmation(self): 89 | self.handler = self.capture_list 90 | self.by_id('accountContentIframe').frame_window().call('submitCheckout', 'renewall', 'renewall') 91 | 92 | def capture_list(self): 93 | loans_iframe = self.by_id('accountContentIframe').frame_body() 94 | 95 | loans = loans_iframe.for_each('tr[@class="patFuncEntry"]').map( 96 | barcode='td[@class="patFuncBarcode"]', 97 | key='span[@class="patFuncTitleMain"]', 98 | status='td[@class="patFuncStatus"]' 99 | ).evaluate_with_json() 100 | #print('RESULT', result) 101 | 102 | title_list = list(loans.keys()) 103 | for title in sorted(title_list): 104 | r = reminders.Reminder(self.db) 105 | r.title = title 106 | r.notes = loans[title]['barcode'].strip() + ' - ' + loans[title]['status'].strip() 107 | if title in self.checked_titles: 108 | r.completed = True 109 | r.save() 110 | 111 | self.logout() 112 | 113 | def logout(self): 114 | self.handler = self.logged_out 115 | self.load_url('https://haku.helmet.fi:443/iii/mobile/logoutFilterRedirect?suite=mobile') 116 | 117 | def logged_out(self): 118 | #js = 'document.body.innerHTML' 119 | #print(self.eval_js(js)) 120 | self.person_index += 1 121 | if self.person_index == len(logins): 122 | self.start() 123 | else: 124 | self.start_per_person() 125 | 126 | def get_reminder_list(self): 127 | all_calendars = reminders.get_all_calendars() 128 | for calendar in all_calendars: 129 | if calendar.title == self.list_name: 130 | return calendar 131 | return self.create_reminder_list() 132 | 133 | def create_reminder_list(self): 134 | new_calendar = reminders.Calendar() 135 | new_calendar.title = self.list_name 136 | new_calendar.save() 137 | return new_calendar 138 | 139 | main_view = None 140 | 141 | AVCaptureSession = ObjCClass('AVCaptureSession') 142 | AVCaptureDevice = ObjCClass('AVCaptureDevice') 143 | AVCaptureDeviceInput = ObjCClass('AVCaptureDeviceInput') 144 | AVCaptureMetadataOutput = ObjCClass('AVCaptureMetadataOutput') 145 | AVCaptureVideoPreviewLayer = ObjCClass('AVCaptureVideoPreviewLayer') 146 | dispatch_get_current_queue = c.dispatch_get_current_queue 147 | dispatch_get_current_queue.restype = c_void_p 148 | 149 | def captureOutput_didOutputMetadataObjects_fromConnection_(_self, _cmd, _output, _metadata_objects, _conn): 150 | global all_books 151 | objects = ObjCInstance(_metadata_objects) 152 | for obj in objects: 153 | try: 154 | s = str(obj.stringValue()) 155 | if s in all_books: 156 | sound.play_effect('digital:PowerUp7') 157 | main_view['label'].text = all_books[s].title 158 | r = all_books[s] 159 | r.completed = True 160 | r.save() 161 | except: 162 | pass 163 | 164 | MetadataDelegate = create_objc_class('MetadataDelegate', methods=[captureOutput_didOutputMetadataObjects_fromConnection_], protocols=['AVCaptureMetadataOutputObjectsDelegate']) 165 | 166 | @on_main_thread 167 | def start_scanning(): 168 | global main_view, all_books 169 | global scraper 170 | db = scraper.get_reminder_list() 171 | for r in reminders.get_reminders(db): 172 | code = r.notes[:r.notes.index(' ')] 173 | all_books[code] = r 174 | delegate = MetadataDelegate.new() 175 | main_view = ui.View(frame=(0, 0, 400, 400)) 176 | main_view.name = 'Kirjaskanneri' 177 | session = AVCaptureSession.alloc().init() 178 | device = AVCaptureDevice.defaultDeviceWithMediaType_('vide') 179 | _input = AVCaptureDeviceInput.deviceInputWithDevice_error_(device, None) 180 | if _input: 181 | session.addInput_(_input) 182 | else: 183 | print('Failed to create input') 184 | return 185 | output = AVCaptureMetadataOutput.alloc().init() 186 | queue = ObjCInstance(dispatch_get_current_queue()) 187 | output.setMetadataObjectsDelegate_queue_(delegate, queue) 188 | session.addOutput_(output) 189 | output.setMetadataObjectTypes_(output.availableMetadataObjectTypes()) 190 | prev_layer = AVCaptureVideoPreviewLayer.layerWithSession_(session) 191 | prev_layer.frame = ObjCInstance(main_view).bounds() 192 | prev_layer.setVideoGravity_('AVLayerVideoGravityResizeAspectFill') 193 | ObjCInstance(main_view).layer().addSublayer_(prev_layer) 194 | label = ui.Label(frame=(0, 0, 400, 30), flex='W', name='label') 195 | label.background_color = (0, 0, 0, 0.5) 196 | label.text_color = 'white' 197 | label.text = 'Nothing scanned yet' 198 | label.alignment = ui.ALIGN_CENTER 199 | main_view.add_subview(label) 200 | session.startRunning() 201 | main_view.present('sheet') 202 | main_view.wait_modal() 203 | session.stopRunning() 204 | delegate.release() 205 | session.release() 206 | output.release() 207 | 208 | if __name__ == '__main__': 209 | all_books = {} 210 | scraper = HelmetScraper() 211 | #scraper.present() 212 | -------------------------------------------------------------------------------- /timer 2.py: -------------------------------------------------------------------------------- 1 | import ui, console, objc_util, keychain 2 | from types import SimpleNamespace as ns 3 | 4 | font = 'Apple SD Gothic Neo' 5 | UIColor = objc_util.ObjCClass('UIColor') 6 | objc_black = UIColor.darkGrayColor().CGColor() 7 | 8 | light_theme = ns(front='black', back='white', secondary='darkgrey', shadow=objc_black) 9 | dark_theme = ns(front='white', back='black', secondary='grey', shadow=objc_black) 10 | blue_theme = ns(front='#1976D2', back='white', secondary='#03A9F4', shadow=objc_black) 11 | green_theme = ns(front='#009688', back='white', secondary='#80CBC4', shadow=objc_black) 12 | red_theme = ns(front='#E53935', back='white', secondary='#FFA726', shadow=objc_black) 13 | cyan_dark_theme = ns(front='#4DD0E1', back='black', secondary='#00897B', shadow=objc_black) 14 | 15 | themes = [light_theme, dark_theme, blue_theme, green_theme, red_theme, cyan_dark_theme] 16 | 17 | try: 18 | current_theme_index = int(keychain.get_password('timer','timer')) 19 | theme = themes[current_theme_index] 20 | except TypeError: 21 | current_theme_index = 0 22 | theme = light_theme 23 | 24 | 25 | class Toucher(ui.View): 26 | 27 | seconds = 60 28 | running = False 29 | threshold = 60 30 | panning = False 31 | prev_y = 0 32 | start_loc = None 33 | 34 | def __init__(self, start_time, **kwargs): 35 | super().__init__(**kwargs) 36 | self.start_time = self.current_time = start_time 37 | 38 | def update(self): 39 | l = self.superview 40 | l.text = str(self.current_time) 41 | if self.current_time == 0: 42 | self.update_interval = 0 43 | self.running = False 44 | self.seconds = 60 45 | l.text_color = theme.secondary 46 | console.set_idle_timer_disabled(False) 47 | return 48 | self.seconds -= 1 49 | self.set_needs_display() 50 | if self.seconds == 0: 51 | self.seconds = 60 52 | self.current_time -= 1 53 | 54 | def draw(self): 55 | ui.set_color(theme.front) 56 | path = ui.Path() 57 | path.line_width = 10 58 | insets = self.objc_instance.safeAreaInsets() 59 | path.move_to(0, self.height-5-insets.bottom) 60 | path.line_to(self.width/60*self.seconds, self.height-5-insets.bottom) 61 | path.stroke() 62 | 63 | def touch_began(self, touch): 64 | self.start_loc = touch.location 65 | self.panning = False 66 | self.prev_y = touch.location[1] 67 | 68 | def touch_moved(self, touch): 69 | l = self.superview 70 | (x,y) = touch.location 71 | if not self.panning: 72 | (px,py) = self.start_loc 73 | if abs(x-px)+abs(y-py) > 40: 74 | self.panning = True 75 | if self.prev_y == 0: 76 | self.prev_y = y 77 | if self.panning: 78 | if not self.running: 79 | delta_y = y - self.prev_y 80 | if abs(delta_y) > self.threshold: 81 | self.prev_y = y 82 | if delta_y > 0: 83 | self.current_time -= 1 84 | else: 85 | self.current_time += 1 86 | if self.current_time > 99: self.current_time = 0 87 | if self.current_time < 0: 88 | self.current_time = self.start_time 89 | l.text = str(self.current_time) 90 | self.seconds = 60 91 | self.set_needs_display() 92 | 93 | def touch_ended(self, touch): 94 | l = self.superview 95 | if not self.panning: 96 | self.running = self.running == False 97 | if self.running: 98 | self.update_interval = 1 99 | l.text_color = theme.front 100 | console.set_idle_timer_disabled(True) 101 | else: 102 | self.update_interval = 0 103 | l.text_color = theme.secondary 104 | console.set_idle_timer_disabled(False) 105 | self.panning = False 106 | self.prev_y = 0 107 | 108 | 109 | max_size = min(ui.get_screen_size()) 110 | timer_font_size = 1000 111 | while timer_font_size > 1: 112 | w, h = ui.measure_string('OO', font=(font, timer_font_size), alignment=ui.ALIGN_CENTER) 113 | if w < max_size: break 114 | #if w/max_size > 1.2: 115 | timer_font_size /= 1.1 116 | #else: 117 | # timer_font_size -= 1 118 | 119 | class TimeSelection(ui.View): 120 | 121 | default_times = ('theme', 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90) 122 | times_per_row = 3 123 | rows = 4 124 | cell_font = timer_font_size/(times_per_row+1) 125 | first_time = True 126 | 127 | def set_drop_shadow(self, color): 128 | self.shadow_opacity = 1 129 | self.shadow_offset = (5,5) 130 | self.shadow_color = color 131 | self.shadow_radius = 5 132 | 133 | def __init__(self, **kwargs): 134 | super().__init__(**kwargs) 135 | self.times = [] 136 | for i in range(len(self.default_times)): 137 | time_value = self.default_times[i] 138 | b = ui.Button( 139 | tint_color='black', 140 | #tint_color='white', 141 | background_color='white', 142 | #border_color='lightgrey', 143 | #border_width=1, 144 | corner_radius=10, 145 | flex='WH') 146 | if time_value == 'theme': 147 | b.image = ui.Image('iob:waterdrop_32') 148 | b.action = self.toggle_theme 149 | else: 150 | b.title = str(time_value) 151 | b.action = self.go_to_timer 152 | #b.objc_instance.setClipsToBounds_(False) 153 | bl = b.objc_instance.layer() 154 | bl.setMasksToBounds_(False) 155 | bl.setShadowOpacity_(1) 156 | bl.setShadowRadius_(1) 157 | bl.setShadowOffset_(objc_util.CGSize(2, 2)) 158 | bl.setShadowColor_(objc_black) 159 | 160 | b.font = (font, self.cell_font) 161 | #if i % 2 == 1: 162 | # b.background_color = 'darkgrey' 163 | self.times.append(b) 164 | self.add_subview(b) 165 | self.set_theme() 166 | 167 | def toggle_theme(self, sender): 168 | global theme, current_theme_index, themes 169 | current_theme_index = (current_theme_index + 1) % len(themes) 170 | theme = themes[current_theme_index] 171 | keychain.set_password('timer', 'timer', str(current_theme_index)) 172 | self.set_theme() 173 | 174 | def set_theme(self): 175 | self.background_color = theme.back 176 | for b in self.times: 177 | b.background_color = theme.back 178 | b.tint_color = theme.front 179 | b.objc_instance.layer().setShadowColor_(theme.shadow) 180 | 181 | def layout(self): 182 | insets = self.objc_instance.safeAreaInsets() 183 | w = self.width - insets.left - insets.right 184 | h = self.height - insets.top - insets.bottom 185 | 186 | cell_width = w/self.times_per_row 187 | cell_height = h/self.rows 188 | #w,h = 0,0 189 | dim = 0 190 | for i in range(len(self.default_times)): 191 | b = self.times[i] 192 | b.size_to_fit() 193 | dim = max(dim, b.width, b.height) 194 | #w = max(w, b.width) 195 | #h = max(h, b.height) 196 | for i in range(len(self.default_times)): 197 | b = self.times[i] 198 | column = i % self.times_per_row 199 | row = int(i/self.times_per_row) 200 | frame = ( 201 | insets.left + column*cell_width, 202 | insets.top + row*cell_height, 203 | cell_width, 204 | cell_height 205 | ) 206 | b.frame = frame 207 | center = b.center 208 | b.width = b.height = dim 209 | b.center = center 210 | #b.frame = b.frame.inset(10,10) 211 | 212 | def go_to_timer(self, sender): 213 | if self.first_time: 214 | self.first_time = False 215 | else: 216 | self.navigation_view.pop_view() 217 | first_start_time = int(sender.title) 218 | l = ui.Label() 219 | l.background_color = theme.back 220 | l.text_color = theme.secondary 221 | l.text = str(first_start_time) 222 | l.alignment = ui.ALIGN_CENTER 223 | 224 | l.font = (font, timer_font_size) 225 | l.touch_enabled = True 226 | t = Toucher(start_time=int(sender.title), frame=l.bounds, flex='WH') 227 | l.add_subview(t) 228 | self.navigation_view.push_view(l) 229 | 230 | m = TimeSelection(flex='WH', hidden='True') 231 | 232 | n = ui.NavigationView(m, navigation_bar_hidden=False, tint_color='white') 233 | n.objc_instance.navigationController().navigationBar().hidden = True 234 | n.present('full_screen', hide_title_bar=True) 235 | 236 | 237 | insets = n.objc_instance.safeAreaInsets() 238 | #n.frame = n.frame.inset(insets.top, insets.left, insets.bottom, insets.right) 239 | 240 | m.hidden = False 241 | -------------------------------------------------------------------------------- /timer.py: -------------------------------------------------------------------------------- 1 | import ui, console, objc_util, keychain 2 | from types import SimpleNamespace as ns 3 | 4 | font = 'Apple SD Gothic Neo' 5 | UIColor = objc_util.ObjCClass('UIColor') 6 | objc_black = UIColor.darkGrayColor().CGColor() 7 | 8 | light_theme = ns(front='black', back='white', secondary='darkgrey', shadow=objc_black) 9 | dark_theme = ns(front='white', back='black', secondary='grey', shadow=objc_black) 10 | blue_theme = ns(front='#1976D2', back='white', secondary='#03A9F4', shadow=objc_black) 11 | green_theme = ns(front='#009688', back='white', secondary='#80CBC4', shadow=objc_black) 12 | red_theme = ns(front='#E53935', back='white', secondary='#FFA726', shadow=objc_black) 13 | cyan_dark_theme = ns(front='#4DD0E1', back='black', secondary='#00897B', shadow=objc_black) 14 | 15 | themes = [light_theme, dark_theme, blue_theme, green_theme, red_theme, cyan_dark_theme] 16 | 17 | try: 18 | current_theme_index = int(keychain.get_password('timer','timer')) 19 | theme = themes[current_theme_index] 20 | except TypeError: 21 | current_theme_index = 0 22 | theme = light_theme 23 | 24 | 25 | class Toucher(ui.View): 26 | 27 | seconds = 60 28 | running = False 29 | threshold = 60 30 | panning = False 31 | prev_y = 0 32 | start_loc = None 33 | 34 | def __init__(self, start_time, **kwargs): 35 | super().__init__(**kwargs) 36 | self.start_time = self.current_time = start_time 37 | 38 | def update(self): 39 | l = self.superview 40 | l.text = str(self.current_time) 41 | if self.current_time == 0: 42 | self.update_interval = 0 43 | self.running = False 44 | self.seconds = 60 45 | l.text_color = theme.secondary 46 | console.set_idle_timer_disabled(False) 47 | return 48 | self.seconds -= 1 49 | self.set_needs_display() 50 | if self.seconds == 0: 51 | self.seconds = 60 52 | self.current_time -= 1 53 | 54 | def draw(self): 55 | ui.set_color(theme.front) 56 | path = ui.Path() 57 | path.line_width = 10 58 | insets = self.objc_instance.safeAreaInsets() 59 | path.move_to(0, self.height-5-insets.bottom) 60 | path.line_to(self.width/60*self.seconds, self.height-5-insets.bottom) 61 | path.stroke() 62 | 63 | def touch_began(self, touch): 64 | self.start_loc = touch.location 65 | self.panning = False 66 | self.prev_y = touch.location[1] 67 | 68 | def touch_moved(self, touch): 69 | l = self.superview 70 | (x,y) = touch.location 71 | if not self.panning: 72 | (px,py) = self.start_loc 73 | if abs(x-px)+abs(y-py) > 40: 74 | self.panning = True 75 | if self.prev_y == 0: 76 | self.prev_y = y 77 | if self.panning: 78 | if not self.running: 79 | delta_y = y - self.prev_y 80 | if abs(delta_y) > self.threshold: 81 | self.prev_y = y 82 | if delta_y > 0: 83 | self.current_time -= 1 84 | else: 85 | self.current_time += 1 86 | if self.current_time > 99: self.current_time = 0 87 | if self.current_time < 0: 88 | self.current_time = self.start_time 89 | l.text = str(self.current_time) 90 | self.seconds = 60 91 | self.set_needs_display() 92 | 93 | def touch_ended(self, touch): 94 | l = self.superview 95 | if not self.panning: 96 | self.running = self.running == False 97 | if self.running: 98 | self.update_interval = 1 99 | l.text_color = theme.front 100 | console.set_idle_timer_disabled(True) 101 | else: 102 | self.update_interval = 0 103 | l.text_color = theme.secondary 104 | console.set_idle_timer_disabled(False) 105 | self.panning = False 106 | self.prev_y = 0 107 | 108 | 109 | max_size = min(ui.get_screen_size()) 110 | timer_font_size = 1000 111 | while timer_font_size > 1: 112 | w, h = ui.measure_string('OO', font=(font, timer_font_size), alignment=ui.ALIGN_CENTER) 113 | if w < max_size: break 114 | #if w/max_size > 1.2: 115 | timer_font_size /= 1.1 116 | #else: 117 | # timer_font_size -= 1 118 | 119 | class TimeSelection(ui.View): 120 | 121 | default_times = ('theme', 1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90) 122 | times_per_row = 3 123 | rows = 4 124 | cell_font = timer_font_size/(times_per_row+1) 125 | first_time = True 126 | 127 | def set_drop_shadow(self, color): 128 | self.shadow_opacity = 1 129 | self.shadow_offset = (5,5) 130 | self.shadow_color = color 131 | self.shadow_radius = 5 132 | 133 | def __init__(self, **kwargs): 134 | super().__init__(**kwargs) 135 | self.times = [] 136 | for i in range(len(self.default_times)): 137 | time_value = self.default_times[i] 138 | b = ui.Button( 139 | tint_color='black', 140 | #tint_color='white', 141 | background_color='white', 142 | #border_color='lightgrey', 143 | #border_width=1, 144 | corner_radius=10, 145 | flex='WH') 146 | if time_value == 'theme': 147 | b.image = ui.Image('iob:waterdrop_32') 148 | b.action = self.toggle_theme 149 | else: 150 | b.title = str(time_value) 151 | b.action = self.go_to_timer 152 | #b.objc_instance.setClipsToBounds_(False) 153 | bl = b.objc_instance.layer() 154 | bl.setMasksToBounds_(False) 155 | bl.setShadowOpacity_(1) 156 | bl.setShadowRadius_(1) 157 | bl.setShadowOffset_(objc_util.CGSize(2, 2)) 158 | bl.setShadowColor_(objc_black) 159 | 160 | b.font = (font, self.cell_font) 161 | #if i % 2 == 1: 162 | # b.background_color = 'darkgrey' 163 | self.times.append(b) 164 | self.add_subview(b) 165 | self.set_theme() 166 | 167 | def toggle_theme(self, sender): 168 | global theme, current_theme_index, themes 169 | current_theme_index = (current_theme_index + 1) % len(themes) 170 | theme = themes[current_theme_index] 171 | keychain.set_password('timer', 'timer', str(current_theme_index)) 172 | self.set_theme() 173 | 174 | def set_theme(self): 175 | self.background_color = theme.back 176 | for b in self.times: 177 | b.background_color = theme.back 178 | b.tint_color = theme.front 179 | b.objc_instance.layer().setShadowColor_(theme.shadow) 180 | 181 | def layout(self): 182 | insets = self.objc_instance.safeAreaInsets() 183 | w = self.width - insets.left - insets.right 184 | h = self.height - insets.top - insets.bottom 185 | 186 | cell_width = w/self.times_per_row 187 | cell_height = h/self.rows 188 | #w,h = 0,0 189 | dim = 0 190 | for i in range(len(self.default_times)): 191 | b = self.times[i] 192 | b.size_to_fit() 193 | dim = max(dim, b.width, b.height) 194 | #w = max(w, b.width) 195 | #h = max(h, b.height) 196 | for i in range(len(self.default_times)): 197 | b = self.times[i] 198 | column = i % self.times_per_row 199 | row = int(i/self.times_per_row) 200 | frame = ( 201 | insets.left + column*cell_width, 202 | insets.top + row*cell_height, 203 | cell_width, 204 | cell_height 205 | ) 206 | b.frame = frame 207 | center = b.center 208 | b.width = b.height = dim 209 | b.center = center 210 | #b.frame = b.frame.inset(10,10) 211 | 212 | def go_to_timer(self, sender): 213 | if self.first_time: 214 | self.first_time = False 215 | else: 216 | self.navigation_view.pop_view() 217 | first_start_time = int(sender.title) 218 | l = ui.Label() 219 | l.background_color = theme.back 220 | l.text_color = theme.secondary 221 | l.text = str(first_start_time) 222 | l.alignment = ui.ALIGN_CENTER 223 | 224 | l.font = (font, timer_font_size) 225 | l.touch_enabled = True 226 | t = Toucher(start_time=int(sender.title), frame=l.bounds, flex='WH') 227 | l.add_subview(t) 228 | self.navigation_view.push_view(l) 229 | 230 | m = TimeSelection(flex='WH', hidden='True') 231 | 232 | n = ui.NavigationView(m, navigation_bar_hidden=False, tint_color='white') 233 | n.objc_instance.navigationController().navigationBar().hidden = True 234 | n.present('fullscreen', hide_title_bar=True) 235 | 236 | 237 | insets = n.objc_instance.safeAreaInsets() 238 | #n.frame = n.frame.inset(insets.top, insets.left, insets.bottom, insets.right) 239 | 240 | m.hidden = False 241 | -------------------------------------------------------------------------------- /pic2text.py: -------------------------------------------------------------------------------- 1 | #language_preference = ['fi'] #,'en','se'] 2 | 3 | import photos, ui, dialogs, clipboard 4 | import io, ctypes 5 | from functools import partial 6 | from objc_util import * 7 | 8 | load_framework('Vision') 9 | VNRecognizeTextRequest = ObjCClass('VNRecognizeTextRequest') 10 | VNImageRequestHandler = ObjCClass('VNImageRequestHandler') 11 | 12 | (picker_photos, picker_camera) = (0, 1) 13 | 14 | UIImagePNGRepresentation = c.UIImagePNGRepresentation 15 | UIImagePNGRepresentation.argtypes = [ctypes.c_void_p] 16 | UIImagePNGRepresentation.restype = ctypes.c_void_p 17 | 18 | UIImage = ObjCClass('UIImage') 19 | UIImageSymbolConfiguration = ObjCClass('UIImageSymbolConfiguration') 20 | 21 | root = ui.View( 22 | tint_color='black', 23 | ) 24 | 25 | results_table = ui.TableView( 26 | allows_multiple_selection=True, 27 | frame=root.bounds, flex='WH', 28 | ) 29 | 30 | #WEIGHTS 31 | ULTRALIGHT, THIN, LIGHT, REGULAR, MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK = range(1, 10) 32 | # SCALES 33 | SMALL, MEDIUM, LARGE = 1, 2, 3 34 | 35 | def SymbolImage(name, point_size=None, weight=None, scale=None): 36 | ''' Create a ui.Image from an SFSymbol name. Optional parameters: 37 | * `point_size` - Integer font size 38 | * `weight` - Font weight, one of ULTRALIGHT, THIN, LIGHT, REGULAR, MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK 39 | * `scale` - Size relative to font size, one of SMALL, MEDIUM, LARGE 40 | 41 | Run the file to see a symbol browser.''' 42 | objc_image = ObjCClass('UIImage').systemImageNamed_(name) 43 | conf = UIImageSymbolConfiguration.defaultConfiguration() 44 | if point_size is not None: 45 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 46 | conf, 47 | UIImageSymbolConfiguration.configurationWithPointSize_(point_size)) 48 | if weight is not None: 49 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 50 | conf, 51 | UIImageSymbolConfiguration.configurationWithWeight_(weight)) 52 | if scale is not None: 53 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 54 | conf, 55 | UIImageSymbolConfiguration.configurationWithScale_(scale)) 56 | objc_image = objc_image.imageByApplyingSymbolConfiguration_(conf) 57 | 58 | return ui.Image.from_data( 59 | nsdata_to_bytes(ObjCInstance(UIImagePNGRepresentation(objc_image))) 60 | ) 61 | 62 | def imagePickerController_didFinishPickingMediaWithInfo_(self,cmd,picker,info): 63 | 64 | global results_table 65 | 66 | pick = ObjCInstance(picker) 67 | pick.setDelegate_(None) 68 | ObjCInstance(self).release() 69 | pick.dismissViewControllerAnimated_completion_(True, None) 70 | 71 | img = ObjCInstance(info)['UIImagePickerControllerEditedImage'] 72 | png_data = ObjCInstance(UIImagePNGRepresentation(img.ptr)) 73 | results_table.data_source.recognize(png_data) 74 | 75 | 76 | SUIViewController = ObjCClass('SUIViewController') 77 | 78 | MyPickerDelegate = create_objc_class('MyPickerDelegate', 79 | methods=[imagePickerController_didFinishPickingMediaWithInfo_], protocols=['UIImagePickerControllerDelegate']) 80 | 81 | class RecognizedTextSource: 82 | 83 | def __init__(self, root, tableview, **kwargs): 84 | super().__init__(**kwargs) 85 | self.tableview = tableview 86 | self.recognized_text = [] 87 | self.selected_rows = set() 88 | 89 | self.camera_button = ui.ButtonItem( 90 | tint_color='black', 91 | image=SymbolImage('camera', 8, weight=THIN), 92 | action=partial( 93 | self.get_photo_action, 94 | picker_camera 95 | ) 96 | ) 97 | self.photos_button = ui.ButtonItem( 98 | tint_color='black', 99 | image=SymbolImage('photo.on.rectangle', 8, weight=THIN), 100 | action=partial( 101 | self.get_photo_action, 102 | picker_photos 103 | ) 104 | ) 105 | self.copy_button = ui.ButtonItem( 106 | tint_color='black', 107 | title='Copy', 108 | enabled=False, 109 | action=self.copy_action 110 | ) 111 | self.share_button = ui.ButtonItem( 112 | tint_color='black', 113 | title='Share', 114 | enabled=False, 115 | action=self.share_action 116 | ) 117 | 118 | root.left_button_items = [ 119 | self.copy_button, 120 | self.share_button, 121 | ] 122 | root.right_button_items = [ 123 | self.camera_button, 124 | self.photos_button, 125 | ] 126 | 127 | @on_main_thread 128 | def get_photo_action(self, picker_type, sender): 129 | picker = ObjCClass('UIImagePickerController').alloc().init() 130 | 131 | delegate = MyPickerDelegate.alloc().init() 132 | picker.setDelegate_(delegate) 133 | 134 | picker.allowsEditing = True 135 | picker.sourceType = picker_type 136 | 137 | vc = SUIViewController.viewControllerForView_( 138 | self.tableview.superview.objc_instance) 139 | vc.presentModalViewController_animated_(picker, True) 140 | 141 | def recognize(self, image_data): 142 | req = VNRecognizeTextRequest.alloc().init().autorelease() 143 | handler = VNImageRequestHandler.alloc().initWithData_options_( 144 | image_data, None 145 | ).autorelease() 146 | success = handler.performRequests_error_([req], None) 147 | if success: 148 | self.recognized_text = [ 149 | str(result.text()) 150 | for result 151 | in req.results() 152 | ] 153 | self.selected_rows = set() 154 | self.copy_button.enabled = True 155 | self.share_button.enabled = True 156 | self.tableview.reload() 157 | else: 158 | self.copy_button.enabled = False 159 | self.share_button.enabled = False 160 | dialogs.hud_alert('Failed to recognize anything') 161 | 162 | def copy_action(self, sender): 163 | text = self.get_text() 164 | if text is None: 165 | return 166 | clipboard.set(text) 167 | dialogs.hud_alert('Copied') 168 | 169 | def share_action(self, sender): 170 | text = self.get_text() 171 | if text is None: 172 | return 173 | dialogs.share_text(text) 174 | 175 | def get_text(self): 176 | if len(self.recognized_text) == 0: 177 | None 178 | if len(self.selected_rows) == 0: 179 | to_combine = self.recognized_text 180 | else: 181 | to_combine = [ 182 | self.recognized_text[i] 183 | for i 184 | in sorted(self.selected_rows) 185 | ] 186 | return '\n'.join(to_combine) 187 | 188 | 189 | def tableview_number_of_rows(self, tableview, section): 190 | return len(self.recognized_text) 191 | 192 | def tableview_cell_for_row(self, tableview, section, row): 193 | cell = ui.TableViewCell() 194 | cell.text_label.text = self.recognized_text[row] 195 | 196 | return cell 197 | 198 | def tableview_did_select(self, tableview, section, row): 199 | self.selected_rows.add(row) 200 | 201 | def tableview_did_deselect(self, tableview, section, row): 202 | self.selected_rows.remove(row) 203 | 204 | results_table.data_source = results_table.delegate = RecognizedTextSource( 205 | root, results_table) 206 | 207 | root.add_subview(results_table) 208 | 209 | root.present() 210 | 211 | ''' 212 | OLD LANGUAGE-RELATED CODE 213 | Nothing but English supported... 214 | 215 | #revision = VNRecognizeTextRequest.currentRevision() 216 | #supported = VNRecognizeTextRequest.supportedRecognitionLanguagesForTextRecognitionLevel_revision_error_(0, revision, None) 217 | #print(supported) 218 | 219 | 220 | OLD PHOTO PICKING CODE 221 | Uses Pythonista modules. Easy, readable and very slow. 222 | 223 | def from_camera(self, sender): 224 | pil_image = None 225 | pil_image = photos.capture_image() 226 | self.convert_image(pil_image) 227 | 228 | def from_photos(self, sender): 229 | pil_image = None 230 | asset = photos.pick_asset() 231 | if asset is not None: 232 | pil_image = asset.get_image() 233 | self.convert_image(pil_image) 234 | 235 | @ui.in_background 236 | def convert_image(self, pil_image): 237 | if pil_image is None: 238 | dialogs.hud_alert('Canceled') 239 | return 240 | dialogs.hud_alert('Converting image') 241 | buffer = io.BytesIO() 242 | pil_image.save(buffer, format='PNG') 243 | image_data = buffer.getvalue() 244 | self.recognize(image_data) 245 | ''' 246 | -------------------------------------------------------------------------------- /gym.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | from ui import * 4 | from anchor import * 5 | import pygestures 6 | 7 | import tinysync 8 | 9 | import functools, types, datetime 10 | 11 | dark_background = '#0e0e0e' 12 | default_tint = '#fffade' # '#e6fbff' 13 | default_highlight = '#7faa6e' 14 | 15 | def style_title(view): 16 | view.background_color = '#202029' 17 | view.text_color = default_highlight 18 | view.tint_color = view.text_color 19 | view.alignment = ALIGN_CENTER 20 | view.line_break_mode = LB_WORD_WRAP 21 | view.font = ('', 18) 22 | if hasattr(view, 'title') and view.title is not None: 23 | view.title = view.title.upper() 24 | if hasattr(view, 'text') and view.text is not None: 25 | view.text = view.text.upper() 26 | return view 27 | 28 | def style_title_transparent(view): 29 | style_title(view) 30 | view.background_color = 'transparent' 31 | return view 32 | 33 | def style_data(view): 34 | view.background_color = '#202029' 35 | view.text_color = default_tint 36 | view.tint_color = default_tint 37 | view.alignment = ALIGN_CENTER 38 | view.font = ('Apple SD Gothic Neo', 32) 39 | view.border_color = view.background_color 40 | return view 41 | 42 | def style_data_transparent(view): 43 | style_data(view) 44 | view.background_color = 'transparent' 45 | return view 46 | 47 | def background_style(view): 48 | view.background_color = dark_background 49 | 50 | 51 | class DaySelection(ui.View): 52 | 53 | def __init__(self, **kwargs): 54 | super().__init__(**kwargs) 55 | self.background_color = default_highlight 56 | 57 | days = [day.title for day in exercises] 58 | grid = GridView( 59 | frame=self.bounds, flex='WH') 60 | for i, day in enumerate(days): 61 | day_button = ui.Button(title=day) 62 | style_title(day_button) 63 | 64 | day_button.day_index = i 65 | day_button.action = self.day_select 66 | grid.add_subview(day_button) 67 | self.add_subview(grid) 68 | 69 | def day_select(self, sender): 70 | e = ExerciseCard( 71 | sender.day_index, 0, 72 | frame=self.bounds, flex='WH', 73 | background_color=dark_background 74 | ) 75 | self.navigation_view.push_view(e) 76 | 77 | 78 | class ExerciseCard(GridView, pygestures.GestureView): 79 | 80 | def __init__(self, 81 | day_index, exercise_index, **kwargs): 82 | super().__init__(**kwargs) 83 | 84 | self.day_index = day_index 85 | self.exercise_index = exercise_index 86 | 87 | exercise = exercises[day_index].exercises[exercise_index] 88 | 89 | #grid = GridView(frame=self.bounds, flex='WH') 90 | 91 | title_label = style_title(Autofit_Label( 92 | 32, 93 | text=exercise.title, 94 | number_of_lines=0)) 95 | self.add_subview(title_label) 96 | #title_label.objc_instance.setAdjustsFontSizeToFitWidth(True) 97 | 98 | iv = style_title( 99 | ImageView(image=ui.Image( 100 | 'gym-images/'+exercise.image_name, 101 | with_rendering_mode=ui.CONTENT_SCALE_ASPECT_FIT 102 | )) 103 | if exercise.image_name is not '' 104 | else ui.View() 105 | ) 106 | reps = style_title(Label(text=exercise.reps)) 107 | iv.add_subview(reps) 108 | reps.dock.bottom_trailing() 109 | self.add_subview(iv) 110 | 111 | self.add_subview(style_title( 112 | NotesSection('Mikael', exercise)) 113 | if 'weight_mikael' in exercise 114 | else ui.View() 115 | ) 116 | self.add_subview(style_title( 117 | NotesSection('Oskari', exercise)) 118 | if 'weight_oskari' in exercise 119 | else ui.View() 120 | ) 121 | 122 | self.done_mikael = Round_Button( 123 | image=ui.Image('iob:ios7_checkmark_empty_256'), 124 | tint_color='grey', 125 | border_color='grey', 126 | border_width=1) 127 | self.done_mikael.action = functools.partial(self.completed, 'Mikael', exercise) 128 | self.add_subview(self.done_mikael) 129 | 130 | self.done_oskari = Round_Button( 131 | image=ui.Image('iob:ios7_checkmark_empty_256'), 132 | tint_color='grey', 133 | border_color='grey', 134 | border_width=1) 135 | self.done_oskari.action = functools.partial(self.completed, 'Oskari', exercise) 136 | self.add_subview(self.done_oskari) 137 | 138 | def completed(self, name, exercise, sender): 139 | weight = getattr(exercise, 'weight_'+name.lower()) 140 | title = exercise.title.replace(' ', '_') 141 | today = datetime.datetime.now() 142 | with open('gym-log', 'a', encoding='utf-8') as fp: 143 | fp.write(f'{today:%Y-%m-%d} {name} {title} {weight}\n') 144 | sender.background_color = default_highlight 145 | sender.border_color = default_highlight 146 | sender.tint_color = 'white' 147 | 148 | def on_edge_swipe_left(self, data): 149 | ex_index = self.exercise_index + 1 150 | if ex_index < len(exercises[self.day_index].exercises): 151 | e = ExerciseCard( 152 | self.day_index, ex_index, 153 | frame=self.bounds, flex='WH', 154 | background_color=dark_background 155 | ) 156 | else: 157 | e = DaySelection() 158 | self.navigation_view.push_view(e) 159 | 160 | 161 | class NotesSection(ui.View): 162 | 163 | def __init__(self, trainer_name, exercise, **kwargs): 164 | super().__init__(**kwargs) 165 | enable(self) 166 | 167 | self.exercise = exercise 168 | #exercise = exercises[day_index].exercises[exercise_index] 169 | #self.day_index = day_index 170 | #self.exercise_index = exercise_index 171 | self.attr = 'weight_'+trainer_name.lower() 172 | 173 | self.trainer = Label(text=trainer_name) 174 | style_title(self.trainer) 175 | 176 | self.weight = TextField( 177 | text=str(exercise[self.attr]), 178 | clear_button_mode='while_editing', 179 | keyboard_type=KEYBOARD_NUMBERS, 180 | bordered=False, 181 | action=self.edited) 182 | style_data_transparent(self.weight) 183 | 184 | add_subviews(self) 185 | 186 | self.trainer.dock.bottom_leading() 187 | self.weight.dock.all() 188 | 189 | def edited(self, sender): 190 | #print(self.day_index, self.exercise_index, self.attr) 191 | #try: 192 | new_weight = sender.text 193 | #except: 194 | #exercise = exercises[self.day_index].exercises[self.exercise_index] 195 | #sender.text = str(self.exercise[self.attr]) 196 | #return 197 | self.exercise[self.attr] = new_weight 198 | #exercises[self.day_index].exercises[self.exercise_index][self.attr] = new_weight 199 | 200 | #print(exercises) 201 | 202 | 203 | def relay(attribute_name): 204 | '''Property creator for pass-through properties''' 205 | p = property( 206 | lambda self: 207 | getattr(self.target, attribute_name), 208 | lambda self, value: 209 | setattr(self.target, attribute_name, value) 210 | ) 211 | return p 212 | 213 | 214 | class PassthruView(ui.View): 215 | 216 | def __new__(cls, *args, **kwargs): 217 | _, ui_cls_name = cls.__name__.split('_') 218 | ui_cls = getattr(ui, ui_cls_name) 219 | t = cls.target = ui_cls() 220 | for key in ui_cls.__dict__: 221 | if not key.startswith('_'): 222 | setattr(cls, key, relay(key)) 223 | instance = super().__new__(cls, *args, **kwargs) 224 | instance.target = t 225 | t.frame = instance.bounds 226 | t.flex = 'WH' 227 | instance.add_subview(t) 228 | #cls.__init__(instance, *args, **kwargs) 229 | return instance 230 | 231 | 232 | class Round_Button(PassthruView): 233 | 234 | background_color = relay('background_color') 235 | border_color = relay('border_color') 236 | border_width = relay('border_width') 237 | tint_color = relay('tint_color') 238 | 239 | def layout(self): 240 | self.target.width = self.target.height = min(self.width, self.height)/2 241 | self.target.center = self.bounds.center() 242 | self.target.corner_radius = self.target.width/2 243 | 244 | 245 | class Autofit_Label(PassthruView): 246 | 247 | def __init__(self, font_max_size, **kwargs): 248 | self.font_max_size = font_max_size 249 | super().__init__(**kwargs) 250 | 251 | ''' 252 | alignment = relay('alignment') 253 | font = relay('font') 254 | line_break_mode = relay('line_break_mode') 255 | number_of_lines = relay('number_of_lines') 256 | text = relay('text') 257 | text_color = relay('text_color') 258 | ''' 259 | 260 | def layout(self): 261 | font_name, _ = self.font 262 | font_size = self.font_max_size 263 | 264 | while True: 265 | by_word_ok = True 266 | for word in self.text.split(): 267 | w,h = ui.measure_string( 268 | word, 269 | max_width=0, 270 | font=(font_name, font_size), 271 | ) 272 | if w > self.width - 16: 273 | by_word_ok = False 274 | break 275 | if not by_word_ok: 276 | font_size -= 1 277 | continue 278 | w,h = ui.measure_string( 279 | self.text, 280 | max_width=self.width, 281 | font=(font_name, font_size), 282 | alignment=self.alignment 283 | ) 284 | if h > self.height - 16: 285 | font_size -= 1 286 | continue 287 | break 288 | 289 | self.font = (font_name, font_size) 290 | 291 | 292 | if __name__ == '__main__': 293 | 294 | exercises = tinysync.track([], name='gym-program') 295 | 296 | v = View(background_color=dark_background) 297 | 298 | d = DaySelection() 299 | 300 | n = NavigationView( 301 | d, 302 | navigation_bar_hidden=False, 303 | #tint_color=default_tint, 304 | frame=v.bounds, flex='WH') 305 | n.objc_instance.navigationController().navigationBar().hidden = True 306 | 307 | v.add_subview(n) 308 | n.dock.all(fit=Dock.SAFE) 309 | 310 | v.present( 311 | 'full_screen', 312 | animated=False, 313 | hide_title_bar=True, 314 | title_bar_color='white', 315 | orientations=['portrait']) 316 | -------------------------------------------------------------------------------- /gym 2.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | from ui import * 4 | from anchor import * 5 | import pygestures 6 | 7 | import tinysync 8 | 9 | import functools, types, datetime 10 | 11 | dark_background = '#0e0e0e' 12 | default_tint = '#fffade' # '#e6fbff' 13 | default_highlight = '#7faa6e' 14 | 15 | def style_title(view): 16 | view.background_color = '#202029' 17 | view.text_color = default_highlight 18 | view.tint_color = view.text_color 19 | view.alignment = ALIGN_CENTER 20 | view.line_break_mode = LB_WORD_WRAP 21 | view.font = ('', 18) 22 | if hasattr(view, 'title') and view.title is not None: 23 | view.title = view.title.upper() 24 | if hasattr(view, 'text') and view.text is not None: 25 | view.text = view.text.upper() 26 | return view 27 | 28 | def style_title_transparent(view): 29 | style_title(view) 30 | view.background_color = 'transparent' 31 | return view 32 | 33 | def style_data(view): 34 | view.background_color = '#202029' 35 | view.text_color = default_tint 36 | view.tint_color = default_tint 37 | view.alignment = ALIGN_CENTER 38 | view.font = ('Apple SD Gothic Neo', 32) 39 | view.border_color = view.background_color 40 | return view 41 | 42 | def style_data_transparent(view): 43 | style_data(view) 44 | view.background_color = 'transparent' 45 | return view 46 | 47 | def background_style(view): 48 | view.background_color = dark_background 49 | 50 | 51 | class DaySelection(ui.View): 52 | 53 | def __init__(self, **kwargs): 54 | super().__init__(**kwargs) 55 | self.background_color = default_highlight 56 | 57 | days = [day.title for day in exercises] 58 | grid = GridView( 59 | frame=self.bounds, flex='WH') 60 | for i, day in enumerate(days): 61 | day_button = ui.Button(title=day) 62 | style_title(day_button) 63 | 64 | day_button.day_index = i 65 | day_button.action = self.day_select 66 | grid.add_subview(day_button) 67 | self.add_subview(grid) 68 | 69 | def day_select(self, sender): 70 | e = ExerciseCard( 71 | sender.day_index, 0, 72 | frame=self.bounds, flex='WH', 73 | background_color=dark_background 74 | ) 75 | self.navigation_view.push_view(e) 76 | 77 | 78 | class ExerciseCard(GridView, pygestures.GestureView): 79 | 80 | def __init__(self, 81 | day_index, exercise_index, **kwargs): 82 | super().__init__(**kwargs) 83 | 84 | self.day_index = day_index 85 | self.exercise_index = exercise_index 86 | 87 | exercise = exercises[day_index].exercises[exercise_index] 88 | 89 | #grid = GridView(frame=self.bounds, flex='WH') 90 | 91 | title_label = style_title(Autofit_Label( 92 | 32, 93 | text=exercise.title, 94 | number_of_lines=0)) 95 | self.add_subview(title_label) 96 | #title_label.objc_instance.setAdjustsFontSizeToFitWidth(True) 97 | 98 | iv = style_title( 99 | ImageView(image=ui.Image( 100 | 'gym-images/'+exercise.image_name, 101 | with_rendering_mode=ui.CONTENT_SCALE_ASPECT_FIT 102 | )) 103 | if exercise.image_name is not '' 104 | else ui.View() 105 | ) 106 | reps = style_title(Label(text=exercise.reps)) 107 | iv.add_subview(reps) 108 | reps.dock.bottom_trailing() 109 | self.add_subview(iv) 110 | 111 | self.add_subview(style_title( 112 | NotesSection('Mikael', exercise)) 113 | if 'weight_mikael' in exercise 114 | else ui.View() 115 | ) 116 | self.add_subview(style_title( 117 | NotesSection('Oskari', exercise)) 118 | if 'weight_oskari' in exercise 119 | else ui.View() 120 | ) 121 | 122 | self.done_mikael = Round_Button( 123 | image=ui.Image('iob:ios7_checkmark_empty_256'), 124 | tint_color='grey', 125 | border_color='grey', 126 | border_width=1) 127 | self.done_mikael.action = functools.partial(self.completed, 'Mikael', exercise) 128 | self.add_subview(self.done_mikael) 129 | 130 | self.done_oskari = Round_Button( 131 | image=ui.Image('iob:ios7_checkmark_empty_256'), 132 | tint_color='grey', 133 | border_color='grey', 134 | border_width=1) 135 | self.done_oskari.action = functools.partial(self.completed, 'Oskari', exercise) 136 | self.add_subview(self.done_oskari) 137 | 138 | def completed(self, name, exercise, sender): 139 | weight = getattr(exercise, 'weight_'+name.lower()) 140 | title = exercise.title.replace(' ', '_') 141 | today = datetime.datetime.now() 142 | with open('gym-log', 'a', encoding='utf-8') as fp: 143 | fp.write(f'{today:%Y-%m-%d} {name} {title} {weight}\n') 144 | sender.background_color = default_highlight 145 | sender.border_color = default_highlight 146 | sender.tint_color = 'white' 147 | 148 | def on_edge_swipe_left(self, data): 149 | ex_index = self.exercise_index + 1 150 | if ex_index < len(exercises[self.day_index].exercises): 151 | e = ExerciseCard( 152 | self.day_index, ex_index, 153 | frame=self.bounds, flex='WH', 154 | background_color=dark_background 155 | ) 156 | else: 157 | e = DaySelection() 158 | self.navigation_view.push_view(e) 159 | 160 | 161 | class NotesSection(ui.View): 162 | 163 | def __init__(self, trainer_name, exercise, **kwargs): 164 | super().__init__(**kwargs) 165 | enable(self) 166 | 167 | self.exercise = exercise 168 | #exercise = exercises[day_index].exercises[exercise_index] 169 | #self.day_index = day_index 170 | #self.exercise_index = exercise_index 171 | self.attr = 'weight_'+trainer_name.lower() 172 | 173 | self.trainer = Label(text=trainer_name) 174 | style_title(self.trainer) 175 | 176 | self.weight = TextField( 177 | text=str(exercise[self.attr]), 178 | clear_button_mode='while_editing', 179 | keyboard_type=KEYBOARD_NUMBERS, 180 | bordered=False, 181 | action=self.edited) 182 | style_data_transparent(self.weight) 183 | 184 | add_subviews(self) 185 | 186 | self.trainer.dock.bottom_leading() 187 | self.weight.dock.all() 188 | 189 | def edited(self, sender): 190 | #print(self.day_index, self.exercise_index, self.attr) 191 | try: 192 | new_weight = int(sender.text) 193 | except: 194 | #exercise = exercises[self.day_index].exercises[self.exercise_index] 195 | sender.text = str(self.exercise[self.attr]) 196 | return 197 | self.exercise[self.attr] = new_weight 198 | #exercises[self.day_index].exercises[self.exercise_index][self.attr] = new_weight 199 | 200 | #print(exercises) 201 | 202 | 203 | def relay(attribute_name): 204 | '''Property creator for pass-through properties''' 205 | p = property( 206 | lambda self: 207 | getattr(self.target, attribute_name), 208 | lambda self, value: 209 | setattr(self.target, attribute_name, value) 210 | ) 211 | return p 212 | 213 | 214 | class PassthruView(ui.View): 215 | 216 | def __new__(cls, *args, **kwargs): 217 | _, ui_cls_name = cls.__name__.split('_') 218 | ui_cls = getattr(ui, ui_cls_name) 219 | t = cls.target = ui_cls() 220 | for key in ui_cls.__dict__: 221 | if not key.startswith('_'): 222 | setattr(cls, key, relay(key)) 223 | instance = super().__new__(cls, *args, **kwargs) 224 | instance.target = t 225 | t.frame = instance.bounds 226 | t.flex = 'WH' 227 | instance.add_subview(t) 228 | #cls.__init__(instance, *args, **kwargs) 229 | return instance 230 | 231 | 232 | class Round_Button(PassthruView): 233 | 234 | background_color = relay('background_color') 235 | border_color = relay('border_color') 236 | border_width = relay('border_width') 237 | tint_color = relay('tint_color') 238 | 239 | def layout(self): 240 | self.target.width = self.target.height = min(self.width, self.height)/2 241 | self.target.center = self.bounds.center() 242 | self.target.corner_radius = self.target.width/2 243 | 244 | 245 | class Autofit_Label(PassthruView): 246 | 247 | def __init__(self, font_max_size, **kwargs): 248 | self.font_max_size = font_max_size 249 | super().__init__(**kwargs) 250 | 251 | ''' 252 | alignment = relay('alignment') 253 | font = relay('font') 254 | line_break_mode = relay('line_break_mode') 255 | number_of_lines = relay('number_of_lines') 256 | text = relay('text') 257 | text_color = relay('text_color') 258 | ''' 259 | 260 | def layout(self): 261 | font_name, _ = self.font 262 | font_size = self.font_max_size 263 | 264 | while True: 265 | by_word_ok = True 266 | for word in self.text.split(): 267 | w,h = ui.measure_string( 268 | word, 269 | max_width=0, 270 | font=(font_name, font_size), 271 | ) 272 | if w > self.width - 16: 273 | by_word_ok = False 274 | break 275 | if not by_word_ok: 276 | font_size -= 1 277 | continue 278 | w,h = ui.measure_string( 279 | self.text, 280 | max_width=self.width, 281 | font=(font_name, font_size), 282 | alignment=self.alignment 283 | ) 284 | if h > self.height - 16: 285 | font_size -= 1 286 | continue 287 | break 288 | 289 | self.font = (font_name, font_size) 290 | 291 | 292 | if __name__ == '__main__': 293 | 294 | exercises = tinysync.track([], name='gym-program') 295 | 296 | v = View(background_color=dark_background) 297 | 298 | d = DaySelection() 299 | 300 | n = NavigationView( 301 | d, 302 | navigation_bar_hidden=False, 303 | #tint_color=default_tint, 304 | frame=v.bounds, flex='WH') 305 | n.objc_instance.navigationController().navigationBar().hidden = True 306 | 307 | v.add_subview(n) 308 | n.dock.all(fit=Dock.SAFE) 309 | 310 | v.present( 311 | 'full_screen', 312 | animated=False, 313 | hide_title_bar=True, 314 | title_bar_color='white', 315 | orientations=['portrait']) 316 | -------------------------------------------------------------------------------- /gym-07-Jul-2020.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | from ui import * 4 | from anchor import * 5 | import pygestures 6 | 7 | import tinysync 8 | 9 | import functools, types, datetime 10 | 11 | dark_background = '#0e0e0e' 12 | default_tint = '#fffade' # '#e6fbff' 13 | default_highlight = '#7faa6e' 14 | 15 | def style_title(view): 16 | view.background_color = '#202029' 17 | view.text_color = default_highlight 18 | view.tint_color = view.text_color 19 | view.alignment = ALIGN_CENTER 20 | view.line_break_mode = LB_WORD_WRAP 21 | view.font = ('', 18) 22 | if hasattr(view, 'title') and view.title is not None: 23 | view.title = view.title.upper() 24 | if hasattr(view, 'text') and view.text is not None: 25 | view.text = view.text.upper() 26 | return view 27 | 28 | def style_title_transparent(view): 29 | style_title(view) 30 | view.background_color = 'transparent' 31 | return view 32 | 33 | def style_data(view): 34 | view.background_color = '#202029' 35 | view.text_color = default_tint 36 | view.tint_color = default_tint 37 | view.alignment = ALIGN_CENTER 38 | view.font = ('Apple SD Gothic Neo', 32) 39 | view.border_color = view.background_color 40 | return view 41 | 42 | def style_data_transparent(view): 43 | style_data(view) 44 | view.background_color = 'transparent' 45 | return view 46 | 47 | def background_style(view): 48 | view.background_color = dark_background 49 | 50 | 51 | class DaySelection(ui.View): 52 | 53 | def __init__(self, **kwargs): 54 | super().__init__(**kwargs) 55 | self.background_color = default_highlight 56 | 57 | days = [day.title for day in exercises] 58 | grid = GridView( 59 | frame=self.bounds, flex='WH') 60 | for i, day in enumerate(days): 61 | day_button = ui.Button(title=day) 62 | style_title(day_button) 63 | 64 | day_button.day_index = i 65 | day_button.action = self.day_select 66 | grid.add_subview(day_button) 67 | self.add_subview(grid) 68 | 69 | def day_select(self, sender): 70 | e = ExerciseCard( 71 | sender.day_index, 0, 72 | frame=self.bounds, flex='WH', 73 | background_color=dark_background 74 | ) 75 | self.navigation_view.push_view(e) 76 | 77 | 78 | class ExerciseCard(GridView, pygestures.GestureView): 79 | 80 | def __init__(self, 81 | day_index, exercise_index, **kwargs): 82 | super().__init__(**kwargs) 83 | 84 | self.day_index = day_index 85 | self.exercise_index = exercise_index 86 | 87 | exercise = exercises[day_index].exercises[exercise_index] 88 | 89 | #grid = GridView(frame=self.bounds, flex='WH') 90 | 91 | title_label = style_title(Autofit_Label( 92 | 32, 93 | text=exercise.title, 94 | number_of_lines=0)) 95 | self.add_subview(title_label) 96 | #title_label.objc_instance.setAdjustsFontSizeToFitWidth(True) 97 | 98 | iv = style_title( 99 | ImageView(image=ui.Image( 100 | 'gym-images/'+exercise.image_name, 101 | with_rendering_mode=ui.CONTENT_SCALE_ASPECT_FIT 102 | )) 103 | if exercise.image_name is not '' 104 | else ui.View() 105 | ) 106 | reps = style_title(Label(text=exercise.reps)) 107 | iv.add_subview(reps) 108 | reps.dock.bottom_trailing() 109 | self.add_subview(iv) 110 | 111 | self.add_subview(style_title( 112 | NotesSection('Mikael', exercise)) 113 | if 'weight_mikael' in exercise 114 | else ui.View() 115 | ) 116 | self.add_subview(style_title( 117 | NotesSection('Oskari', exercise)) 118 | if 'weight_oskari' in exercise 119 | else ui.View() 120 | ) 121 | 122 | self.done_mikael = Round_Button( 123 | image=ui.Image('iob:ios7_checkmark_empty_256'), 124 | tint_color='grey', 125 | border_color='grey', 126 | border_width=1) 127 | self.done_mikael.action = functools.partial(self.completed, 'Mikael', exercise) 128 | self.add_subview(self.done_mikael) 129 | 130 | self.done_oskari = Round_Button( 131 | image=ui.Image('iob:ios7_checkmark_empty_256'), 132 | tint_color='grey', 133 | border_color='grey', 134 | border_width=1) 135 | self.done_oskari.action = functools.partial(self.completed, 'Oskari', exercise) 136 | self.add_subview(self.done_oskari) 137 | 138 | def completed(self, name, exercise, sender): 139 | weight = getattr(exercise, 'weight_'+name.lower()) 140 | title = exercise.title.replace(' ', '_') 141 | today = datetime.datetime.now() 142 | with open('gym-log', 'a', encoding='utf-8') as fp: 143 | fp.write(f'{today:%Y-%m-%d} {name} {title} {weight}\n') 144 | sender.background_color = default_highlight 145 | sender.border_color = default_highlight 146 | sender.tint_color = 'white' 147 | 148 | def on_edge_swipe_left(self, data): 149 | ex_index = self.exercise_index + 1 150 | if ex_index < len(exercises[self.day_index].exercises): 151 | e = ExerciseCard( 152 | self.day_index, ex_index, 153 | frame=self.bounds, flex='WH', 154 | background_color=dark_background 155 | ) 156 | else: 157 | e = DaySelection() 158 | self.navigation_view.push_view(e) 159 | 160 | 161 | class NotesSection(ui.View): 162 | 163 | def __init__(self, trainer_name, exercise, **kwargs): 164 | super().__init__(**kwargs) 165 | enable(self) 166 | 167 | self.exercise = exercise 168 | #exercise = exercises[day_index].exercises[exercise_index] 169 | #self.day_index = day_index 170 | #self.exercise_index = exercise_index 171 | self.attr = 'weight_'+trainer_name.lower() 172 | 173 | self.trainer = Label(text=trainer_name) 174 | style_title(self.trainer) 175 | 176 | self.weight = TextField( 177 | text=str(exercise[self.attr]), 178 | clear_button_mode='while_editing', 179 | keyboard_type=KEYBOARD_NUMBERS, 180 | bordered=False, 181 | action=self.edited) 182 | style_data_transparent(self.weight) 183 | 184 | add_subviews(self) 185 | 186 | self.trainer.dock.bottom_leading() 187 | self.weight.dock.all() 188 | 189 | def edited(self, sender): 190 | #print(self.day_index, self.exercise_index, self.attr) 191 | #try: 192 | new_weight = sender.text 193 | #except: 194 | #exercise = exercises[self.day_index].exercises[self.exercise_index] 195 | #sender.text = str(self.exercise[self.attr]) 196 | #return 197 | self.exercise[self.attr] = new_weight 198 | #exercises[self.day_index].exercises[self.exercise_index][self.attr] = new_weight 199 | 200 | #print(exercises) 201 | 202 | 203 | def relay(attribute_name): 204 | '''Property creator for pass-through properties''' 205 | p = property( 206 | lambda self: 207 | getattr(self.target, attribute_name), 208 | lambda self, value: 209 | setattr(self.target, attribute_name, value) 210 | ) 211 | return p 212 | 213 | 214 | class PassthruView(ui.View): 215 | 216 | def __new__(cls, *args, **kwargs): 217 | _, ui_cls_name = cls.__name__.split('_') 218 | ui_cls = getattr(ui, ui_cls_name) 219 | t = cls.target = ui_cls() 220 | for key in ui_cls.__dict__: 221 | if not key.startswith('_'): 222 | setattr(cls, key, relay(key)) 223 | instance = super().__new__(cls, *args, **kwargs) 224 | instance.target = t 225 | t.frame = instance.bounds 226 | t.flex = 'WH' 227 | instance.add_subview(t) 228 | #cls.__init__(instance, *args, **kwargs) 229 | return instance 230 | 231 | 232 | class Round_Button(PassthruView): 233 | 234 | background_color = relay('background_color') 235 | border_color = relay('border_color') 236 | border_width = relay('border_width') 237 | tint_color = relay('tint_color') 238 | 239 | def layout(self): 240 | self.target.width = self.target.height = min(self.width, self.height)/2 241 | self.target.center = self.bounds.center() 242 | self.target.corner_radius = self.target.width/2 243 | 244 | 245 | class Autofit_Label(PassthruView): 246 | 247 | def __init__(self, font_max_size, **kwargs): 248 | self.font_max_size = font_max_size 249 | super().__init__(**kwargs) 250 | 251 | ''' 252 | alignment = relay('alignment') 253 | font = relay('font') 254 | line_break_mode = relay('line_break_mode') 255 | number_of_lines = relay('number_of_lines') 256 | text = relay('text') 257 | text_color = relay('text_color') 258 | ''' 259 | 260 | def layout(self): 261 | font_name, _ = self.font 262 | font_size = self.font_max_size 263 | 264 | while True: 265 | by_word_ok = True 266 | for word in self.text.split(): 267 | w,h = ui.measure_string( 268 | word, 269 | max_width=0, 270 | font=(font_name, font_size), 271 | ) 272 | if w > self.width - 16: 273 | by_word_ok = False 274 | break 275 | if not by_word_ok: 276 | font_size -= 1 277 | continue 278 | w,h = ui.measure_string( 279 | self.text, 280 | max_width=self.width, 281 | font=(font_name, font_size), 282 | alignment=self.alignment 283 | ) 284 | if h > self.height - 16: 285 | font_size -= 1 286 | continue 287 | break 288 | 289 | self.font = (font_name, font_size) 290 | 291 | 292 | if __name__ == '__main__': 293 | 294 | exercises = tinysync.track([], name='gym-program') 295 | 296 | v = View(background_color=dark_background) 297 | 298 | d = DaySelection() 299 | 300 | n = NavigationView( 301 | d, 302 | navigation_bar_hidden=False, 303 | #tint_color=default_tint, 304 | frame=v.bounds, flex='WH') 305 | n.objc_instance.navigationController().navigationBar().hidden = True 306 | 307 | v.add_subview(n) 308 | n.dock.all(fit=Dock.SAFE) 309 | 310 | v.present( 311 | 'full_screen', 312 | animated=False, 313 | hide_title_bar=True, 314 | title_bar_color='white', 315 | orientations=['portrait']) 316 | -------------------------------------------------------------------------------- /sfsymbol 2.py: -------------------------------------------------------------------------------- 1 | 2 | import ui, clipboard, re, dialogs 3 | from objc_util import * 4 | 5 | UIImage = ObjCClass('UIImage') 6 | UIImageSymbolConfiguration = ObjCClass('UIImageSymbolConfiguration') 7 | 8 | UIImagePNGRepresentation = c.UIImagePNGRepresentation 9 | UIImagePNGRepresentation.restype = c_void_p 10 | UIImagePNGRepresentation.argtypes = [c_void_p] 11 | 12 | #WEIGHTS 13 | ULTRALIGHT, THIN, LIGHT, REGULAR, MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK = range(1, 10) 14 | # SCALES 15 | SMALL, MEDIUM, LARGE = 1, 2, 3 16 | 17 | def SymbolImage(name, point_size=None, weight=None, scale=None): 18 | ''' Create a ui.Image from an SFSymbol name. Optional parameters: 19 | * `point_size` - Integer font size 20 | * `weight` - Font weight, one of ULTRALIGHT, THIN, LIGHT, REGULAR, MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK 21 | * `scale` - Size relative to font size, one of SMALL, MEDIUM, LARGE 22 | 23 | Run the file to see a symbol browser.''' 24 | objc_image = ObjCClass('UIImage').systemImageNamed_(name) 25 | conf = UIImageSymbolConfiguration.defaultConfiguration() 26 | if point_size is not None: 27 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 28 | conf, 29 | UIImageSymbolConfiguration.configurationWithPointSize_(point_size)) 30 | if weight is not None: 31 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 32 | conf, 33 | UIImageSymbolConfiguration.configurationWithWeight_(weight)) 34 | if scale is not None: 35 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 36 | conf, 37 | UIImageSymbolConfiguration.configurationWithScale_(scale)) 38 | objc_image = objc_image.imageByApplyingSymbolConfiguration_(conf) 39 | 40 | return ui.Image.from_data( 41 | nsdata_to_bytes(ObjCInstance(UIImagePNGRepresentation(objc_image))) 42 | ) 43 | 44 | 45 | if __name__ == '__main__': 46 | 47 | class SymbolSource: 48 | 49 | symbols_per_page = 20 50 | 51 | def __init__(self, root, tableview): 52 | self.tableview = tableview 53 | tableview.row_height = 50 54 | self.weight = THIN 55 | 56 | with open('sfsymbolnames.txt', 'r') as fp: 57 | all_lines = fp.read() 58 | raw = all_lines.splitlines() 59 | 60 | restricted_prefix = 'Usage restricted' 61 | 62 | self.symbol_names = [] 63 | for i, symbol_name in enumerate(raw): 64 | if raw[i].startswith(restricted_prefix): continue 65 | if i+1 == len(raw): continue 66 | value = symbol_name 67 | if raw[i+1].startswith(restricted_prefix): 68 | value = 'R ' + value 69 | self.symbol_names.append(value) 70 | 71 | self.index = 0 72 | self.update_list_to_display() 73 | 74 | self.prev_button = ui.ButtonItem( 75 | tint_color='black', 76 | image=SymbolImage('arrow.left', 8, weight=THIN), 77 | enabled=False, 78 | action=self.prev, 79 | ) 80 | self.to_start_button = ui.ButtonItem( 81 | tint_color='black', 82 | image=SymbolImage('arrow.left.to.line', 8, weight=THIN), 83 | enabled=False, 84 | action=self.to_start, 85 | ) 86 | self.next_button = ui.ButtonItem( 87 | tint_color='black', 88 | image=SymbolImage('arrow.right', 8, weight=THIN), 89 | enabled=True, 90 | action=self.next, 91 | ) 92 | self.to_end_button = ui.ButtonItem( 93 | tint_color='black', 94 | image=SymbolImage('arrow.right.to.line', 8, weight=THIN), 95 | enabled=True, 96 | action=self.to_end, 97 | ) 98 | self.weight_button = ui.ButtonItem( 99 | tint_color='black', 100 | title='Thin', 101 | enabled=True, 102 | action=self.change_weight, 103 | ) 104 | 105 | root.left_button_items = [ 106 | self.to_start_button, 107 | self.prev_button] 108 | root.right_button_items = [ 109 | self.to_end_button, 110 | self.next_button, 111 | self.weight_button] 112 | 113 | def update_list_to_display(self): 114 | self.data_list = [] 115 | for i in range(self.index, self.index+self.symbols_per_page): 116 | self.data_list.append(self.symbol_names[i]) 117 | 118 | def next(self, sender): 119 | self.index += self.symbols_per_page 120 | if self.index + self.symbols_per_page >= len(self.symbol_names): 121 | self.index = len(self.symbol_names) - self.symbols_per_page - 1 122 | self.next_button.enabled = False 123 | self.to_end_button.enabled = False 124 | self.prev_button.enabled = True 125 | self.to_start_button.enabled = True 126 | self.update_list_to_display() 127 | self.tableview.reload() 128 | 129 | def to_end(self, sender): 130 | self.index = len(self.symbol_names) - self.symbols_per_page - 1 131 | self.next_button.enabled = False 132 | self.to_end_button.enabled = False 133 | self.prev_button.enabled = True 134 | self.to_start_button.enabled = True 135 | self.update_list_to_display() 136 | self.tableview.reload() 137 | 138 | def prev(self, sender): 139 | self.index -= self.symbols_per_page 140 | if self.index <= 0: 141 | self.index = 0 142 | self.prev_button.enabled = False 143 | self.to_start_button.enabled = False 144 | self.next_button.enabled = True 145 | self.to_end_button.enabled = True 146 | self.update_list_to_display() 147 | self.tableview.reload() 148 | 149 | def to_start(self, sender): 150 | self.index = 0 151 | self.prev_button.enabled = False 152 | self.to_start_button.enabled = False 153 | self.next_button.enabled = True 154 | self.to_end_button.enabled = True 155 | self.update_list_to_display() 156 | self.tableview.reload() 157 | 158 | def change_weight(self, sender): 159 | titles = ['Ultralight', 'Thin', 'Light', 'Regular', 'Medium', 'Semibold', 'Bold', 'Heavy', 'Black'] 160 | self.weight += 1 161 | if self.weight > BLACK: 162 | self.weight = ULTRALIGHT 163 | self.weight_button.title = titles[self.weight-1] 164 | self.tableview.reload() 165 | 166 | def tableview_number_of_rows(self, tableview, section): 167 | return len(self.data_list) 168 | 169 | def tableview_cell_for_row(self, tableview, section, row): 170 | cell = ui.TableViewCell() 171 | cell.selectable = False 172 | cell.background_color='black' 173 | 174 | symbol_name = self.data_list[row] 175 | tint_color = 'white' 176 | if symbol_name.startswith('R '): 177 | symbol_name = symbol_name[2:] 178 | tint_color = 'orange' 179 | symbol_image = SymbolImage(symbol_name, 180 | point_size=14, weight=self.weight, scale=SMALL) 181 | 182 | button = ui.Button( 183 | tint_color=tint_color, 184 | title=' '+symbol_name, 185 | font=('Fira Mono', 14), 186 | image=symbol_image, 187 | frame=cell.content_view.bounds, 188 | flex='WH', 189 | action=self.copy_to_clipboard, 190 | #enabled=False, 191 | ) 192 | cell.content_view.add_subview(button) 193 | 194 | return cell 195 | 196 | def copy_to_clipboard(self, sender): 197 | clipboard.set(sender.title[3:]) 198 | dialogs.hud_alert('Copied') 199 | 200 | def textfield_did_change(self, textfield): 201 | search_text = textfield.text.strip().lower() 202 | if search_text == '': 203 | self.update_list_to_display() 204 | textfield.end_editing() 205 | else: 206 | self.data_list = list(fuzzyfinder(search_text, self.symbol_names)) 207 | self.tableview.reload() 208 | 209 | def fuzzyfinder(input, collection, accessor=lambda x: x, sort_results=True): 210 | suggestions = [] 211 | input = str(input) if not isinstance(input, str) else input 212 | pat = '.*?'.join(map(re.escape, input)) 213 | pat = '(?=({0}))'.format(pat) 214 | regex = re.compile(pat, re.IGNORECASE) 215 | for item in collection: 216 | r = list(regex.finditer(accessor(item))) 217 | if r: 218 | best = min(r, key=lambda x: len(x.group(1))) 219 | suggestions.append((len(best.group(1)), best.start(), accessor(item), item)) 220 | if sort_results: 221 | return (z[-1] for z in sorted(suggestions)) 222 | else: 223 | return (z[-1] for z in sorted(suggestions, key=lambda x: x[:2])) 224 | 225 | root = ui.View() 226 | 227 | symbol_table = ui.TableView( 228 | background_color='black', 229 | frame=root.bounds, flex='WH', 230 | ) 231 | data_source = symbol_table.data_source = SymbolSource(root, symbol_table) 232 | 233 | search_field = ui.TextField( 234 | frame=(8,8, root.width-16, 40), 235 | flex='W', 236 | clear_button_mode='always', 237 | delegate=data_source, 238 | ) 239 | symbol_table.y = search_field.height + 16 240 | symbol_table.height -= (search_field.height + 16) 241 | 242 | root.add_subview(search_field) 243 | root.add_subview(symbol_table) 244 | 245 | #symbol_table.present() 246 | root.present('fullscreen') 247 | 248 | -------------------------------------------------------------------------------- /sfsymbol.py: -------------------------------------------------------------------------------- 1 | 2 | import ui, clipboard, re, dialogs 3 | from objc_util import * 4 | 5 | UIImage = ObjCClass('UIImage') 6 | UIImageSymbolConfiguration = ObjCClass('UIImageSymbolConfiguration') 7 | 8 | UIImagePNGRepresentation = c.UIImagePNGRepresentation 9 | UIImagePNGRepresentation.restype = c_void_p 10 | UIImagePNGRepresentation.argtypes = [c_void_p] 11 | 12 | #WEIGHTS 13 | ULTRALIGHT, THIN, LIGHT, REGULAR, MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK = range(1, 10) 14 | # SCALES 15 | SMALL, MEDIUM, LARGE = 1, 2, 3 16 | 17 | def SymbolImage(name, point_size=None, weight=None, scale=None): 18 | ''' Create a ui.Image from an SFSymbol name. Optional parameters: 19 | * `point_size` - Integer font size 20 | * `weight` - Font weight, one of ULTRALIGHT, THIN, LIGHT, REGULAR, 21 | MEDIUM, SEMIBOLD, BOLD, HEAVY, BLACK 22 | * `scale` - Size relative to font size, one of SMALL, MEDIUM, LARGE 23 | 24 | Run the file to see a symbol browser.''' 25 | objc_image = ObjCClass('UIImage').systemImageNamed_(name) 26 | conf = UIImageSymbolConfiguration.defaultConfiguration() 27 | if point_size is not None: 28 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 29 | conf, 30 | UIImageSymbolConfiguration.configurationWithPointSize_(point_size)) 31 | if weight is not None: 32 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 33 | conf, 34 | UIImageSymbolConfiguration.configurationWithWeight_(weight)) 35 | if scale is not None: 36 | conf = UIImageSymbolConfiguration.configurationWithConfiguration_and_( 37 | conf, 38 | UIImageSymbolConfiguration.configurationWithScale_(scale)) 39 | objc_image = objc_image.imageByApplyingSymbolConfiguration_(conf) 40 | 41 | return ui.Image.from_data( 42 | nsdata_to_bytes(ObjCInstance(UIImagePNGRepresentation(objc_image))) 43 | ) 44 | 45 | 46 | if __name__ == '__main__': 47 | 48 | class SymbolSource: 49 | 50 | symbols_per_page = 20 51 | 52 | def __init__(self, root, tableview): 53 | self.tableview = tableview 54 | tableview.row_height = 50 55 | self.weight = THIN 56 | 57 | with open('sfsymbolnames.txt', 'r') as fp: 58 | all_lines = fp.read() 59 | raw = all_lines.splitlines() 60 | 61 | restricted_prefix = 'Usage restricted' 62 | 63 | self.symbol_names = [] 64 | for i, symbol_name in enumerate(raw): 65 | if raw[i].startswith(restricted_prefix): continue 66 | if i+1 == len(raw): continue 67 | value = symbol_name 68 | if raw[i+1].startswith(restricted_prefix): 69 | value = 'R ' + value 70 | self.symbol_names.append(value) 71 | 72 | self.index = 0 73 | self.update_list_to_display() 74 | 75 | self.prev_button = ui.ButtonItem( 76 | tint_color='black', 77 | image=SymbolImage('arrow.left', 8, weight=THIN), 78 | enabled=False, 79 | action=self.prev, 80 | ) 81 | self.to_start_button = ui.ButtonItem( 82 | tint_color='black', 83 | image=SymbolImage('arrow.left.to.line', 8, weight=THIN), 84 | enabled=False, 85 | action=self.to_start, 86 | ) 87 | self.next_button = ui.ButtonItem( 88 | tint_color='black', 89 | image=SymbolImage('arrow.right', 8, weight=THIN), 90 | enabled=True, 91 | action=self.next, 92 | ) 93 | self.to_end_button = ui.ButtonItem( 94 | tint_color='black', 95 | image=SymbolImage('arrow.right.to.line', 8, weight=THIN), 96 | enabled=True, 97 | action=self.to_end, 98 | ) 99 | self.weight_button = ui.ButtonItem( 100 | tint_color='black', 101 | title='Thin', 102 | enabled=True, 103 | action=self.change_weight, 104 | ) 105 | 106 | root.left_button_items = [ 107 | self.to_start_button, 108 | self.prev_button] 109 | root.right_button_items = [ 110 | self.to_end_button, 111 | self.next_button, 112 | self.weight_button] 113 | 114 | def update_list_to_display(self): 115 | self.data_list = [] 116 | for i in range(self.index, self.index+self.symbols_per_page): 117 | self.data_list.append(self.symbol_names[i]) 118 | 119 | def next(self, sender): 120 | self.index += self.symbols_per_page 121 | if self.index + self.symbols_per_page >= len(self.symbol_names): 122 | self.index = len(self.symbol_names) - self.symbols_per_page - 1 123 | self.next_button.enabled = False 124 | self.to_end_button.enabled = False 125 | self.prev_button.enabled = True 126 | self.to_start_button.enabled = True 127 | self.update_list_to_display() 128 | self.tableview.reload() 129 | 130 | def to_end(self, sender): 131 | self.index = len(self.symbol_names) - self.symbols_per_page - 1 132 | self.next_button.enabled = False 133 | self.to_end_button.enabled = False 134 | self.prev_button.enabled = True 135 | self.to_start_button.enabled = True 136 | self.update_list_to_display() 137 | self.tableview.reload() 138 | 139 | def prev(self, sender): 140 | self.index -= self.symbols_per_page 141 | if self.index <= 0: 142 | self.index = 0 143 | self.prev_button.enabled = False 144 | self.to_start_button.enabled = False 145 | self.next_button.enabled = True 146 | self.to_end_button.enabled = True 147 | self.update_list_to_display() 148 | self.tableview.reload() 149 | 150 | def to_start(self, sender): 151 | self.index = 0 152 | self.prev_button.enabled = False 153 | self.to_start_button.enabled = False 154 | self.next_button.enabled = True 155 | self.to_end_button.enabled = True 156 | self.update_list_to_display() 157 | self.tableview.reload() 158 | 159 | def change_weight(self, sender): 160 | titles = ['Ultralight', 'Thin', 'Light', 'Regular', 'Medium', 'Semibold', 'Bold', 'Heavy', 'Black'] 161 | self.weight += 1 162 | if self.weight > BLACK: 163 | self.weight = ULTRALIGHT 164 | self.weight_button.title = titles[self.weight-1] 165 | self.tableview.reload() 166 | 167 | def tableview_number_of_rows(self, tableview, section): 168 | return len(self.data_list) 169 | 170 | def tableview_cell_for_row(self, tableview, section, row): 171 | cell = ui.TableViewCell() 172 | cell.selectable = False 173 | cell.background_color='black' 174 | 175 | symbol_name = self.data_list[row] 176 | tint_color = 'white' 177 | if symbol_name.startswith('R '): 178 | symbol_name = symbol_name[2:] 179 | tint_color = 'orange' 180 | symbol_image = SymbolImage(symbol_name, 181 | point_size=14, weight=self.weight, scale=SMALL) 182 | 183 | button = ui.Button( 184 | tint_color=tint_color, 185 | title=' '+symbol_name, 186 | font=('Fira Mono', 14), 187 | image=symbol_image, 188 | frame=cell.content_view.bounds, 189 | flex='WH', 190 | action=self.copy_to_clipboard, 191 | #enabled=False, 192 | ) 193 | cell.content_view.add_subview(button) 194 | 195 | return cell 196 | 197 | def copy_to_clipboard(self, sender): 198 | clipboard.set(sender.title[3:]) 199 | dialogs.hud_alert('Copied') 200 | 201 | def textfield_did_change(self, textfield): 202 | search_text = textfield.text.strip().lower() 203 | if search_text == '': 204 | self.update_list_to_display() 205 | textfield.end_editing() 206 | else: 207 | self.data_list = list(fuzzyfinder(search_text, self.symbol_names)) 208 | self.tableview.reload() 209 | 210 | def fuzzyfinder(input, collection, accessor=lambda x: x, sort_results=True): 211 | suggestions = [] 212 | input = str(input) if not isinstance(input, str) else input 213 | pat = '.*?'.join(map(re.escape, input)) 214 | pat = '(?=({0}))'.format(pat) 215 | regex = re.compile(pat, re.IGNORECASE) 216 | for item in collection: 217 | r = list(regex.finditer(accessor(item))) 218 | if r: 219 | best = min(r, key=lambda x: len(x.group(1))) 220 | suggestions.append((len(best.group(1)), best.start(), accessor(item), item)) 221 | if sort_results: 222 | return (z[-1] for z in sorted(suggestions)) 223 | else: 224 | return (z[-1] for z in sorted(suggestions, key=lambda x: x[:2])) 225 | 226 | root = ui.View() 227 | 228 | symbol_table = ui.TableView( 229 | background_color='black', 230 | frame=root.bounds, flex='WH', 231 | ) 232 | data_source = symbol_table.data_source = SymbolSource(root, symbol_table) 233 | 234 | search_field = ui.TextField( 235 | frame=(8,8, root.width-16, 40), 236 | flex='W', 237 | clear_button_mode='always', 238 | delegate=data_source, 239 | ) 240 | symbol_table.y = search_field.height + 16 241 | symbol_table.height -= (search_field.height + 16) 242 | 243 | root.add_subview(search_field) 244 | root.add_subview(symbol_table) 245 | 246 | #symbol_table.present() 247 | root.present('fullscreen') 248 | 249 | -------------------------------------------------------------------------------- /randomseat.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | from ui import * 3 | import anchor 4 | import random 5 | from scripter import * 6 | import dialogs 7 | 8 | quotes = [ 9 | ('Olkoon Voima kanssanne', 'Arporaattori'), 10 | ('Do, or do not. There is no try.', 'Yoda'), 11 | ('These are not the seats you are looking for', 'Obi-Wan Kenobi'), 12 | ('Everything is proceeding as I have foreseen','The Emperor'), 13 | ('Help me Arporaattori, you are my only hope', 'Princess Leia'), 14 | ('Money leads to tickets; tickets lead to seats; seats lead to prizes', 'Yoda'), 15 | ('I find your lack of faith disturbing', 'Darth Vader'), 16 | ('Baby Yoda! Baby Yoda!', 'The Internet'), 17 | ('I’ve got a bad feeling about this.', 'Han Solo'), 18 | ('Never tell me the odds', 'Han Solo'), 19 | ] 20 | 21 | @script 22 | def wait_for_tap(view): 23 | t = WaitForTap(view) 24 | while not t.tapped: 25 | yield 26 | 27 | 28 | class WaitForTap(View): 29 | 30 | def __init__(self, target, **kwargs): 31 | super().__init__(**kwargs) 32 | self.tapped = False 33 | self.background_color = (0,0,0,0.0001) 34 | self.frame=target.bounds 35 | target.add_subview(self) 36 | 37 | def touch_ended(self, touch): 38 | self.tapped = True 39 | 40 | 41 | class QuoteView(View): 42 | 43 | def __init__(self, quote_index, **kwargs): 44 | super().__init__(**kwargs) 45 | self.background_color = (0,0,0,0.8) 46 | anchor.enable(self) 47 | quote = anchor.Label( 48 | text=quotes[quote_index][0]+'\n', 49 | font=('Savoye LET', 64), 50 | text_color='white', 51 | number_of_lines=0, 52 | alignment=ALIGN_CENTER 53 | ) 54 | 55 | attribution = anchor.Label( 56 | text='- '+quotes[quote_index][1].upper(), 57 | font=('Apple SD Gothic Neo', 20), 58 | text_color='white', 59 | alignment=ALIGN_RIGHT 60 | ) 61 | 62 | self.add_subview(quote) 63 | self.add_subview(attribution) 64 | 65 | quote.at.width == self.at.width * 0.7 66 | quote.at.center_x == self.at.center_x 67 | quote.at.center_y == self.at.center_y 68 | 69 | attribution.at.trailing == quote.at.trailing 70 | attribution.at.top == quote.at.bottom_padding 71 | 72 | @script 73 | def touch_ended(self, touch): 74 | hide(self) 75 | yield 76 | self.superview.run_lottery() 77 | 78 | 79 | class WinningSeatView(View): 80 | 81 | def __init__(self, **kwargs): 82 | super().__init__(**kwargs) 83 | self.background_color = (0,0,0,0.8) 84 | anchor.enable(self) 85 | 86 | row_label = anchor.Label( 87 | text=f'Rivi'.upper(), 88 | font=('Apple SD Gothic Neo', 16), 89 | text_color='white', 90 | alignment=ALIGN_RIGHT 91 | ) 92 | 93 | row_number = self.row = anchor.Label( 94 | font=('Apple SD Gothic Neo', 48), 95 | text_color='white', 96 | number_of_lines=0, 97 | alignment=ALIGN_CENTER 98 | ) 99 | seat_label = anchor.Label( 100 | text='PAIKKA', 101 | font=('Apple SD Gothic Neo', 16), 102 | text_color='white', 103 | alignment=ALIGN_RIGHT 104 | ) 105 | seat_number = self.seat = anchor.Label( 106 | font=('Apple SD Gothic Neo', 48), 107 | text_color='white', 108 | alignment=ALIGN_CENTER 109 | ) 110 | 111 | for view in (seat_label,seat_number,row_label,row_number): 112 | self.add_subview(view) 113 | self.align.center_x(row_number, seat_number) 114 | 115 | row_number.at.width >= seat_number.at.width 116 | seat_number.at.width >= row_number.at.width 117 | row_number.at.bottom == self.at.center_y 118 | seat_number.at.top == row_number.at.bottom_padding 119 | row_label.at.first_baseline == row_number.at.first_baseline 120 | row_label.at.trailing = row_number.at.leading_padding 121 | seat_label.at.first_baseline == seat_number.at.first_baseline 122 | seat_label.at.trailing = seat_number.at.leading_padding 123 | 124 | def set_values(self, row, seat): 125 | self.row.text=f'{row}\n' 126 | self.seat.text=f'{seat}' 127 | 128 | 129 | class CustomView(View): 130 | 131 | seat_map = [ 132 | (2,13), 133 | (1,15), 134 | (1,15), 135 | (1,15), 136 | (2,14), 137 | (1,15), 138 | (2,14), 139 | (2,14), 140 | (2,14), 141 | (1,15), 142 | ] 143 | 144 | def __init__(self, image, **kwargs): 145 | super().__init__(**kwargs) 146 | self.seat_count = sum([last - first + 1 for (first, last) in self.seat_map]) 147 | self.image = image 148 | self.quote_index = 0 149 | self.results_view = WinningSeatView( 150 | frame=self.bounds, flex='WH', 151 | alpha = 0 152 | ) 153 | self.add_subview(self.results_view) 154 | 155 | def layout(self): 156 | (iw, ih) = self.image.size 157 | 158 | if self.width < self.height: 159 | img_width = self.width 160 | img_height = self.width/iw*ih 161 | else: 162 | img_width = self.height/ih*iw 163 | img_height = self.height 164 | 165 | self.cover_x = (self.width - img_width)/2 + img_width/5.6 166 | self.cover_y = (self.height - img_width)/2 + img_width/12 167 | cover_width = img_width*0.61 168 | cover_height = img_width*0.77 169 | self.x_incr = cover_width/15 170 | self.y_incr = cover_height/10 171 | 172 | def seat_coords(self, row, seat): 173 | lead = self.seat_map[row][0] 174 | return ( 175 | (15-seat-lead)*self.x_incr+self.cover_x, 176 | (9-row)*self.y_incr+self.cover_y 177 | ) 178 | 179 | def random_seat(self): 180 | row = random.randint(0,9) 181 | seat = random.randint(0,self.seat_map[row][1]-1) 182 | return (row, seat) 183 | 184 | def next_seat(self, row, seat): 185 | seat += 1 186 | if seat == self.seat_map[row][1]: 187 | seat = 0 188 | row += 1 189 | if row == 10: 190 | row = 0 191 | return (row,seat) 192 | 193 | @script 194 | def kick_off(self): 195 | wait_for_tap(self) 196 | yield 197 | self.show_quote() 198 | 199 | @script 200 | def show_quote(self): 201 | quote_index = 0 202 | ''' 203 | dialogs.alert('Arporaattori', '', quotes[quote_index], hide_cancel_button=True) 204 | ''' 205 | 206 | quote_view = QuoteView(self.quote_index, 207 | frame=self.bounds, 208 | flex='WH', 209 | alpha=0.0) 210 | self.quote_index += 1 211 | self.add_subview(quote_view) 212 | show(quote_view) 213 | ''' 214 | quote_view.present('full_screen', 215 | hide_title_bar=True, 216 | animated=False) 217 | quote_view.wait_modal() 218 | ''' 219 | 220 | @script 221 | def run_lottery(self): 222 | (row,seat) = self.random_seat() 223 | (x,y) = self.seat_coords(row,seat) 224 | spot = Label( 225 | #background_color=(.81, .35, .35, 0.9), 226 | border_color='red', 227 | text_color=(0,0,0,0), 228 | border_width=4, 229 | corner_radius=10, 230 | alignment=ALIGN_CENTER, 231 | number_of_lines=0, 232 | font=('Apple SD Gothic Neo', 48), 233 | frame=(-10,-10,self.width+20,self.height+20)) 234 | self.add_subview(spot) 235 | move(spot, x, y) 236 | slide_value(spot, 'width', self.x_incr) 237 | slide_value(spot, 'height', self.y_incr) 238 | yield 239 | 240 | seats_left = seats_to_move = int((1.0 + random.random() * 2.0) * self.seat_count) 241 | slow_down_threshold = random.randint(self.seat_count//4, self.seat_count//2) 242 | 243 | step_time = 1/120 244 | while seats_left > 0: 245 | seats_left -= 1 246 | if seats_left < slow_down_threshold: 247 | fraction = seats_left/slow_down_threshold 248 | step_time = 1/120 + (1.0-ease_out(fraction)) * 22/45 249 | prev_row = row 250 | (row,seat) = c.next_seat(row,seat) 251 | (x,y) = c.seat_coords(row,seat) 252 | if prev_row == row: 253 | move(spot,x,y, duration=step_time) 254 | yield 255 | else: 256 | spot.x = x 257 | spot.y = y 258 | #if random.random() < step_time: 259 | # step_time += 1/30 260 | timer(spot, 1.0) 261 | yield 262 | self.remove_subview(spot) 263 | ''' 264 | spot.text = f'rivi\n{row+1}\n\npaikka\n{seat+1}' 265 | slide_color(spot, 'text_color', 'white') 266 | slide_value(spot, 'width', self.width) 267 | slide_value(spot, 'height', self.height) 268 | center(spot, self.bounds.center()) 269 | slide_color(spot, 'background_color', (0,0,0,0.8)) 270 | slide_color(spot, 'border_color', (0,0,0,0)) 271 | slide_value(spot, 'corner_radius', 0) 272 | yield 273 | ''' 274 | self.results_view.set_values(row+1, seat+1) 275 | show(self.results_view) 276 | wait_for_tap(self) 277 | yield 278 | hide(self.results_view) 279 | 280 | ''' 281 | seat_view = WinningSeatView(row+1, seat+1, 282 | frame=self.bounds, 283 | flex='WH', 284 | alpha=0.0) 285 | self.add_subview(seat_view) 286 | show(seat_view) 287 | ''' 288 | #dialogs.alert(f'Rivi {row+1}, paikka {seat+1}', '', 'OK', hide_cancel_button=True) 289 | self.show_quote() 290 | 291 | 292 | if __name__ == '__main__': 293 | img = Image('sali9.jpg') 294 | v = ImageView() 295 | v.content_mode = CONTENT_SCALE_ASPECT_FIT 296 | v.image = img 297 | 298 | v.background_color = 'black' 299 | v.present('full_screen', hide_title_bar=True) 300 | 301 | c = CustomView(img, frame=v.bounds, flex='WH') 302 | v.add_subview(c) 303 | 304 | c.layout() 305 | 306 | (row,seat) = c.random_seat() 307 | (x,y) = c.seat_coords(row,seat) 308 | 309 | c.kick_off() 310 | -------------------------------------------------------------------------------- /uiutils 2.py: -------------------------------------------------------------------------------- 1 | ''' Utility functions and classes for Pythonista (iOS app) ui module. ''' 2 | 3 | from itertools import groupby 4 | from more_itertools import chunked, collapse 5 | import keyword 6 | 7 | import ui 8 | 9 | 10 | def add_subviews(view, *subviews): 11 | ''' Helper to add several subviews at once. 12 | Subviews can be provided as comma-separated arguments: 13 | 14 | add_subviews(view, subview1, subview2) 15 | 16 | ... or in an iterable: 17 | 18 | subviews = (subview1, subview2) 19 | add_subviews(view, subviews) 20 | ''' 21 | for subview in collapse(subviews): 22 | view.add_subview(subview) 23 | 24 | def apply(view, **kwargs): 25 | ''' Applies named parameters as changes to the view's attributes. ''' 26 | for key in kwargs: 27 | setattr(view, key, kwargs[key]) 28 | 29 | def apply_down(view, include_self=True, **kwargs): 30 | ''' Applies named parameter as changes to the view's attributes, then 31 | applies them also to the hierarchy of the view's subviews. 32 | Set `include_self` to `False` to only apply the changes to subviews. ''' 33 | if include_self: 34 | apply(view, **kwargs) 35 | for subview in view.subviews: 36 | apply_down(subview, **kwargs) 37 | 38 | 39 | class GridView(ui.View): 40 | """ 41 | Places subviews as squares that fill the available space. 42 | """ 43 | 44 | FILL = 'III' 45 | SPREAD = '___' 46 | CENTER = '_I_' 47 | START = 'II_' 48 | END = '_II' 49 | SIDES = 'I_I' 50 | START_SPREAD = 'I__' 51 | END_SPREAD = '__I' 52 | 53 | MARGIN = At.standard 54 | TIGHT = 0 55 | 56 | def __init__(self, 57 | pack_x=None, pack_y=None, pack=CENTER, 58 | count_x=None, count_y=None, 59 | gap=MARGIN, **kwargs): 60 | '''By default, subviews are laid out in a grid as squares of optimal size and 61 | centered in the view. 62 | 63 | You can fix the amount of views in either dimension with the `count_x` or 64 | `count_y` parameter, or change the packing behaviour by providing 65 | the `pack` parameter with one of the following values: 66 | 67 | * `CENTER` - Clustered in the center (the default) 68 | * `SPREAD` - Distributed evenly 69 | * `FILL` - Fill the available space with only margins in between 70 | (no longer squares) 71 | * `LEADING, TRAILING` (`pack_x` only) 72 | * `TOP, BOTTOM` (`pack_y` only) 73 | ''' 74 | 75 | super().__init__(**kwargs) 76 | 77 | self.pack_x = pack_x or pack 78 | self.pack_y = pack_y or pack 79 | 80 | self.leading_free = self.pack_x[0] == '_' 81 | self.center_x_free = self.pack_x[1] == '_' 82 | self.trailing_free = self.pack_x[2] == '_' 83 | self.top_free = self.pack_y[0] == '_' 84 | self.center_y_free = self.pack_y[1] == '_' 85 | self.bottom_free = self.pack_y[2] == '_' 86 | 87 | self.count_x = count_x 88 | self.count_y = count_y 89 | 90 | self.gap = gap 91 | 92 | enable(self) 93 | 94 | def dimensions(self, count): 95 | if self.height == 0: 96 | return 1, count 97 | ratio = self.width/self.height 98 | count_x = math.sqrt(count * self.width/self.height) 99 | count_y = math.sqrt(count * self.height/self.width) 100 | operations = ( 101 | (math.floor, math.floor), 102 | (math.floor, math.ceil), 103 | (math.ceil, math.floor), 104 | (math.ceil, math.ceil) 105 | ) 106 | best = None 107 | best_x = None 108 | best_y = None 109 | for oper in operations: 110 | cand_x = oper[0](count_x) 111 | cand_y = oper[1](count_y) 112 | diff = cand_x*cand_y - count 113 | if diff >= 0: 114 | if best is None or diff < best: 115 | best = diff 116 | best_x = cand_x 117 | best_y = cand_y 118 | return (best_x, best_y) 119 | 120 | def layout(self): 121 | count = len(self.subviews) 122 | if count == 0: return 123 | 124 | count_x, count_y = self.count_x, self.count_y 125 | if count_x is None and count_y is None: 126 | count_x, count_y = self.dimensions(count) 127 | elif count_x is None: 128 | count_x = math.ceil(count/count_y) 129 | elif count_y is None: 130 | count_y = math.ceil(count/count_x) 131 | if count > count_x * count_y: 132 | raise ValueError( 133 | f'Fixed counts (x: {count_x}, y: {count_y}) not enough to display all views') 134 | 135 | borders = 2 * self.border_width 136 | 137 | dim_x = (self.width-borders-(count_x+1)*self.gap)/count_x 138 | dim_y = (self.height-borders-(count_y+1)*self.gap)/count_y 139 | 140 | dim = min(dim_x, dim_y) 141 | 142 | px = self.pack_x 143 | exp_pack_x = px[0] + px[1]*(count_x-1) + px[2] 144 | py = self.pack_y 145 | exp_pack_y = py[0] + py[1]*(count_y-1) + py[2] 146 | free_count_x = exp_pack_x.count('_') 147 | free_count_y = exp_pack_y.count('_') 148 | 149 | if free_count_x > 0: 150 | per_free_x = ( 151 | self.width - 152 | borders - 153 | count_x*dim - 154 | (count_x+1-free_count_x)*self.gap)/free_count_x 155 | if free_count_y > 0: 156 | per_free_y = ( 157 | self.height - 158 | borders - 159 | count_y*dim - 160 | (count_y+1-free_count_y)*self.gap)/free_count_y 161 | 162 | real_dim_x = dim_x if free_count_x == 0 else dim 163 | real_dim_y = dim_y if free_count_y == 0 else dim 164 | 165 | subviews = iter(self.subviews) 166 | y = self.border_width + (per_free_y if self.top_free else self.gap) 167 | for row in range(count_y): 168 | x = self.border_width + (per_free_x if self.leading_free else self.gap) 169 | for col in range(count_x): 170 | try: 171 | view = next(subviews) 172 | except StopIteration: 173 | break 174 | view.frame = (x, y, real_dim_x, real_dim_y) 175 | x += real_dim_x + (per_free_x if self.center_x_free else self.gap) 176 | y += real_dim_y + (per_free_y if self.center_y_free else self.gap) 177 | 178 | 179 | class Views(dict): 180 | ''' A class that is used to create a hierarchy of ui views defined by 181 | a tree structure, and with the given constraints. 182 | Also stores the created views in depth-first order. 183 | Views can be accessed equivalently with dict references or as attributes: 184 | 185 | * `views['top']` 186 | * `views.top` 187 | ''' 188 | 189 | def __init__(self): 190 | super().__init__() 191 | self._create_views() 192 | 193 | def view_hierarchy(self): 194 | ''' Sample view hierarchy dictionary: 195 | 196 | { 'root': (ui.View, { 197 | 'top': (ui.View, { 198 | 'search_text': ui.TextField, 199 | 'search_action': ui.Button, 200 | }), 201 | 'middle': ui.View, 202 | 'bottom': (ui.View, { 203 | 'accept': ui.Button, 204 | 'cancel': ui.Button, 205 | }) 206 | }) } 207 | 208 | I.e. view names as keys, view classes as values. 209 | If the value is a tuple instead, the first value must be the view class 210 | and the second value a dictionary for the next level of the view 211 | hierarchy. 212 | 213 | View names must match the requirements for identifiers, and 214 | not be any of the Python keywords or attributes of this class 215 | (inheriting `dict`). ''' 216 | 217 | return ( 'root', ui.View ) 218 | 219 | def view_defaults(self, view): 220 | ''' Views are initialized with no arguments. This method is called 221 | with the initialized view to set any defaults you want. 222 | The base implementation creates black views with 223 | white borders, tint and text. ''' 224 | bg = 'black' 225 | fg = 'white' 226 | view.background_color = bg 227 | view.border_color = fg 228 | view.border_width = 1 229 | view.tint_color = fg 230 | view.text_color = fg 231 | 232 | def set_constraints(self): 233 | ''' After all views have been initialized and included in 234 | the hierarchy, this method is called to set the constraints. 235 | Base implementation does nothing. ''' 236 | pass 237 | 238 | def present(self): 239 | ''' Presents the root view of the hierarchy. The base implementation 240 | is a plain `present()` with no arguments. 241 | Return `self` so that you can combine the call with hierarchy init: 242 | 243 | views = Views().present() 244 | ''' 245 | next(iter(self.values())).present() 246 | return self 247 | 248 | def __getattr__(self, key, oga=object.__getattribute__): 249 | if key in self: 250 | return self[key] 251 | else: 252 | return oga(self, key) 253 | 254 | def _create_views(self): 255 | ''' Method that creates a view hierarchy as specified by the 256 | view hierarchy spec. 257 | Each created view is stored by name in `self`. 258 | ''' 259 | 260 | def recursive_view_generation(view_spec, parent): 261 | if parent is None: 262 | assert len(view_spec) in (2, 3), 'Give exactly one root element' 263 | previous_view = None 264 | for is_subspec, group in groupby(view_spec, lambda x: type(x) is tuple): 265 | if is_subspec: 266 | recursive_view_generation(next(group), previous_view) 267 | continue 268 | for view_name, view_class in chunked(group, 2): 269 | assert ( 270 | view_name.isidentifier() 271 | ), f'{view_name} is not a valid identifier' 272 | assert ( 273 | not keyword.iskeyword(view_name) 274 | ), f'Cannot use a keyword as a view name ({view_name})' 275 | assert ( 276 | not view_name in dir(self) 277 | ), f'{view_name} is a member of Views class' 278 | 279 | previous_view = view = view_class(name=view_name) 280 | if parent: 281 | parent.add_subview(view) 282 | self.view_defaults(view) 283 | self[view_name] = view 284 | if parent is None: 285 | self.set_constraints() 286 | 287 | recursive_view_generation(self.view_hierarchy(), None) 288 | 289 | -------------------------------------------------------------------------------- /jswrapper.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | import ui, json, uuid 3 | 4 | class JSWrapper(): 5 | 6 | def __init__(self, prev, to_add_js='', post=''): 7 | if hasattr(prev, 'target_webview'): 8 | self.target_webview = prev.target_webview 9 | prev_js = prev.js 10 | post_js = prev.post_js 11 | else: 12 | self.target_webview = prev 13 | prev_js = 'elem=document;' 14 | post_js = '' 15 | self.post_js = post 16 | self.js = prev_js + ' ' + to_add_js + post_js 17 | 18 | def alert(self, msg=None): 19 | return JSWrapper(self, f'alert("{(msg + ": ") if msg else ""}" + elem);') 20 | 21 | def debug(self, msg=None): 22 | msg = msg + ': ' if msg else '' 23 | print(msg + self.js) 24 | return self 25 | 26 | def fix(self, expr): 27 | expr = expr.replace('"', "'") 28 | if expr[0] != '.': 29 | expr = './/' + expr 30 | return expr 31 | 32 | def plain(self, text): 33 | js = text 34 | return JSWrapper(self, js) 35 | 36 | def xpath(self, expr): 37 | expr = self.fix(expr) 38 | js = f'xpath_result = document.evaluate("{expr}", elem, null, XPathResult.ANY_TYPE, null); elem = xpath_result.iterateNext();' 39 | return JSWrapper(self, js) 40 | 41 | def value(self, expr=None): 42 | return JSWrapper(self, self.generate_value_js(expr)).evaluate() 43 | 44 | def generate_value_js(self, expr=None): 45 | if expr: 46 | expr = self.fix(expr) 47 | pre_js = 'value_elem = ' + ('elem; ' if not expr else f'document.evaluate("{expr}", elem, null, XPathResult.ANY_TYPE, null).iterateNext(); ') 48 | js = pre_js + f'result = "Element not found"; if (value_elem) {{ xpath_result = document.evaluate("string()", value_elem, null, XPathResult.ANY_TYPE, null); if (xpath_result) {{ result = xpath_result.stringValue; }}; }}; result;' 49 | return js 50 | 51 | 52 | def by_id(self, id): 53 | return JSWrapper(self, f'elem=document.getElementById("{id}");') 54 | 55 | def elem(self): 56 | return self.by_id(self.id) 57 | 58 | def by_name(self, name): 59 | return JSWrapper(self, f'elem = document.getElementsByName("{name}")[0];') 60 | 61 | def set_attribute(self, attr_name, value): 62 | value = str(value) 63 | JSWrapper(self, f'elem.setAttribute("{attr_name}", "{value}")').evaluate() 64 | 65 | def set_attr(self, attr_name, value): 66 | value = str(value) 67 | JSWrapper(self, f'elem.{attr_name} = "{value}";').evaluate() 68 | 69 | def set_content(self, content): 70 | content = str(content) 71 | JSWrapper(self, f'elem.innerHTML = "{content}";').evaluate() 72 | 73 | def append(self, html): 74 | js = f'elem.insertAdjacentHTML("beforeend", "{html}");' 75 | JSWrapper(self, js).evaluate() 76 | 77 | def set_field(self, field_name, value): 78 | self.xpath(f"input[@name='{field_name}']").set_attribute('value', value) 79 | 80 | 81 | def list_each(self, expr): 82 | expr = self.fix(expr) 83 | js = f'result_list = []; nodeset = document.evaluate("{expr}", elem, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); not_found = true;\n while(elem = nodeset.iterateNext(), elem) {{\n not_found = false; { self.generate_value_js() } result_list.push(result); }}; if (not_found) {{ result = "No iterable element found"; }};\n JSON.stringify(result_list);\n\n' 84 | return JSWrapper(self, js).evaluate_with_json() 85 | 86 | 87 | def for_each(self, expr): 88 | expr = self.fix(expr) 89 | js = f'collected_result = {{}}; nodeset = document.evaluate("{expr}", elem, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); n = -1; not_found = true;\n while(n++, elem = nodeset.iterateNext(), elem) {{\n not_found = false; ' 90 | post_js = ' }; if (not_found) { collected_result = "No iterable element found"; }JSON.stringify(collected_result);\n\n' 91 | return JSWrapper(self, js, post_js) 92 | 93 | def map(self, **expr_mappings): 94 | create_dict = 'key' in expr_mappings 95 | js = 'mapping_result = {};' 96 | if create_dict: 97 | js += f'get_key = function() {{ { self.generate_value_js(expr_mappings.pop("key")) }; return result; }}\n js_key = get_key();' 98 | else: 99 | js += 'js_key = n;' 100 | for key in expr_mappings: 101 | expr = expr_mappings[key] 102 | expr = self.fix(expr) 103 | js += f"get_value = function() {{ { self.generate_value_js(expr) } return result; }}\n mapping_result['{key}'] = get_value();" 104 | js += 'collected_result[js_key] = mapping_result;' 105 | return JSWrapper(self, js).evaluate_with_json() 106 | 107 | #def set_string_value(self, value): 108 | #return JSWrapper(self, f'elem.value = "{value}";') 109 | 110 | def dot(self, dot_attributes): 111 | return JSWrapper(self, f'elem = elem.{dot_attributes};') 112 | 113 | def style(self, style_attribute): 114 | value = JSWrapper(self, f'elem.style.{style_attribute};').evaluate() 115 | try: 116 | return float(value.strip('px')) 117 | except ValueError: 118 | return value 119 | 120 | def abs_style(self, style_attribute): 121 | value = JSWrapper(self, f'window.getComputedStyle(elem).{style_attribute};').evaluate() 122 | try: 123 | return float(value.strip('px')) 124 | except ValueError: 125 | return value 126 | 127 | def set_style(self, style_attribute, value): 128 | if type(value) in [int, float]: 129 | value = f'{value}px' 130 | value = f'"{value}"' if type(value) == str else value 131 | JSWrapper(self, f'elem.style.{style_attribute}={value};').evaluate() 132 | 133 | def click(self): 134 | return JSWrapper(self, 'elem.click();').evaluate() 135 | 136 | def html(self): 137 | return JSWrapper(self, 'elem.innerHTML;').evaluate() 138 | 139 | def frame_body(self): 140 | return self.dot('contentDocument.body') 141 | 142 | def frame_window(self): 143 | return self.dot('contentWindow') 144 | 145 | def submit(self): 146 | "Submit selected element, or the first form in the document if nothing selected" 147 | if type(self) is not JSWrapper: 148 | self = self.xpath('//form[1]') 149 | JSWrapper(self, f'elem.submit();').evaluate() 150 | 151 | #TODO: Still valuable to be able to separately set by name? 152 | def set_value_by_name(self, name, value): 153 | self.by_name(name).set_string_value(value).evaluate() 154 | 155 | #TODO: Better ideas for calling JS functions? 156 | def call(self, func_name, *args): 157 | js_args = [f'"{item}"' if type(item) == str else str(item) for item in args] 158 | JSWrapper(self, f'elem.{func_name}({js_args})').evaluate() 159 | 160 | def callback(self, func, delay=1.0): 161 | callback_id = self.target_webview.delegate.set_callback(func) 162 | delay_ms = delay * 1000 163 | js = f'setTimeout(function(){{ window.location.href = "{self.target_webview.delegate.callback_prefix}{callback_id}"; }}, {delay_ms});' 164 | print(js) 165 | JSWrapper(self, js).evaluate() 166 | 167 | def evaluate(self): 168 | #print(self.js) 169 | return self.target_webview.eval_js(self.js) 170 | 171 | def evaluate_with_json(self): 172 | return json.loads(self.evaluate()) 173 | 174 | def to_string(self): 175 | return JSWrapper(self, 'elem.toString();').evaluate() 176 | 177 | 178 | class WebScraper(JSWrapper): 179 | 180 | callback_prefix = 'pythonista-callback://' 181 | callbacks = {} 182 | 183 | def __init__(self, webview): 184 | super().__init__(webview) 185 | webview.delegate = self 186 | self.url_map = { 187 | 'about:blank': self.default 188 | } 189 | self.handler = self.default 190 | 191 | def start(self): 192 | self.handler = self.default 193 | self.load_url('about:blank') 194 | 195 | def webview_did_finish_load(self, webview): 196 | url = webview.eval_js('document.URL') 197 | print('Page:', url) 198 | expected_prefix = self.url_map.get(self.handler, 'No action') 199 | if url.startswith(expected_prefix): 200 | print('Handler:', self.handler.__name__) 201 | self.handler() 202 | 203 | def set_callback(self, func): 204 | callback_id = str(uuid.uuid4())[-12:] 205 | self.callbacks[callback_id] = func 206 | return callback_id 207 | 208 | def webview_should_start_load(self, webview, url, nav_type): 209 | if url.startswith(self.callback_prefix): 210 | callback_id = url[len(self.callback_prefix):] 211 | callback_func = self.callbacks[callback_id] 212 | del self.callbacks[callback_id] 213 | ui.delay(callback_func, 0.001) 214 | return False 215 | return True 216 | 217 | def webview_did_fail_load(self, webview, error_code, error_msg): 218 | if error_code != -999: 219 | print(error_code, error_msg) 220 | 221 | def default(self): 222 | pass 223 | 224 | if __name__ == '__main__': 225 | 226 | html = ''' 227 | 228 | 229 | 230 | 231 | Test document 232 | 233 | 234 |
Text in here
235 |
236 | 237 | 238 | 239 |
240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
A1A2A3
B1B2B3
248 | 249 | 250 | ''' 251 | 252 | 253 | class DemoScraper(WebScraper): 254 | 255 | def __init__(self, webview): 256 | super().__init__(webview) 257 | self.url_map = { 258 | self.login_page: 'applewebdata://', 259 | self.content_page: 'https://www.google.com' 260 | } 261 | self.handler = self.login_page 262 | 263 | def login_page(self): 264 | 265 | assert self.by_id('test').to_string() == '[object HTMLDivElement]' 266 | 267 | assert self.xpath('head/title').to_string() == '[object HTMLTitleElement]' 268 | 269 | assert self.xpath('head/title').value() == 'Test document' 270 | 271 | assert self.value('head/title') == 'Test document' 272 | 273 | assert self.xpath('*[@class="test_class"]').to_string() == '[object HTMLDivElement]' 274 | 275 | test_div = self.by_id('test') 276 | 277 | assert test_div.style('top') == 100.0 278 | 279 | assert test_div.style('backgroundColor') == 'inherit' 280 | 281 | assert test_div.abs_style('backgroundColor') == 'rgb(0, 0, 255)' 282 | 283 | test_div.set_style('left', 5) 284 | 285 | assert test_div.abs_style('left') == 5.0 286 | 287 | names = self.list_each('input/@name') 288 | assert names == [ 'username', 'passwd' ] 289 | 290 | cell_values = self.for_each('table//tr').map( 291 | key='td[1]', 292 | some_value='td[3]' 293 | ) 294 | assert cell_values == {'A1': {'some_value': 'A3'}, 'B1': {'some_value': 'B3'}} 295 | 296 | self.set_field('username', 'your username') 297 | self.set_field('passwd', 'your password') 298 | 299 | self.handler = self.content_page 300 | self.by_name('form1').submit() 301 | 302 | def content_page(self): 303 | print(self.xpath('body').html()) 304 | 305 | 306 | wv = ui.WebView() 307 | ds = DemoScraper(wv) 308 | wv.load_html(html) 309 | wv.present() 310 | -------------------------------------------------------------------------------- /uiutils.py: -------------------------------------------------------------------------------- 1 | ''' Utility functions and classes for Pythonista (iOS app) ui module. ''' 2 | 3 | from itertools import groupby 4 | from more_itertools import chunked, collapse 5 | import keyword 6 | 7 | import ui 8 | import objc_util 9 | 10 | 11 | def add_subviews(view, *subviews): 12 | ''' Helper to add several subviews at once. 13 | Subviews can be provided as comma-separated arguments: 14 | 15 | add_subviews(view, subview1, subview2) 16 | 17 | ... or in an iterable: 18 | 19 | subviews = (subview1, subview2) 20 | add_subviews(view, subviews) 21 | ''' 22 | for subview in collapse(subviews): 23 | view.add_subview(subview) 24 | 25 | def apply(view, **kwargs): 26 | ''' Applies named parameters as changes to the view's attributes. ''' 27 | for key in kwargs: 28 | setattr(view, key, kwargs[key]) 29 | 30 | def apply_down(view, include_self=True, **kwargs): 31 | ''' Applies named parameter as changes to the view's attributes, then 32 | applies them also to the hierarchy of the view's subviews. 33 | Set `include_self` to `False` to only apply the changes to subviews. ''' 34 | if include_self: 35 | apply(view, **kwargs) 36 | for subview in view.subviews: 37 | apply_down(subview, **kwargs) 38 | 39 | def objc_color(self, color): 40 | """ Convert any Pythonista color spec to an ObjC UIColor """ 41 | return objc_util.UIColor.colorWithRed_green_blue_alpha_( 42 | *ui.parse_color(color)) 43 | 44 | def py_color(self, objc_color): 45 | """ Convert ObjC UIColor to Python (r, g, b, a) tuple """ 46 | return tuple([c.floatValue() 47 | for c in 48 | objc_color.arrayFromRGBAComponents()]) 49 | if objc_color else None 50 | 51 | def safe_area(view): 52 | """ Returns an intersection of the view's bounds and the iOS-defined safe 53 | area. Note that if you want to set the view's frame based on the result, 54 | you must first translate it to superview coordinates. """ 55 | insets = view.objc_instance.safeAreaInsets() 56 | return view.bounds.inset(insets.top, insets.left, insets.bottom, insets.right) 57 | 58 | 59 | class GridView(ui.View): 60 | """ 61 | Places subviews as squares that fill the available space. 62 | """ 63 | 64 | FILL = 'III' 65 | SPREAD = '___' 66 | CENTER = '_I_' 67 | START = 'II_' 68 | END = '_II' 69 | SIDES = 'I_I' 70 | START_SPREAD = 'I__' 71 | END_SPREAD = '__I' 72 | 73 | MARGIN = At.standard 74 | TIGHT = 0 75 | 76 | def __init__(self, 77 | pack_x=None, pack_y=None, pack=CENTER, 78 | count_x=None, count_y=None, 79 | gap=MARGIN, **kwargs): 80 | '''By default, subviews are laid out in a grid as squares of optimal size and 81 | centered in the view. 82 | 83 | You can fix the amount of views in either dimension with the `count_x` or 84 | `count_y` parameter, or change the packing behaviour by providing 85 | the `pack` parameter with one of the following values: 86 | 87 | * `CENTER` - Clustered in the center (the default) 88 | * `SPREAD` - Distributed evenly 89 | * `FILL` - Fill the available space with only margins in between 90 | (no longer squares) 91 | * `LEADING, TRAILING` (`pack_x` only) 92 | * `TOP, BOTTOM` (`pack_y` only) 93 | ''' 94 | 95 | super().__init__(**kwargs) 96 | 97 | self.pack_x = pack_x or pack 98 | self.pack_y = pack_y or pack 99 | 100 | self.leading_free = self.pack_x[0] == '_' 101 | self.center_x_free = self.pack_x[1] == '_' 102 | self.trailing_free = self.pack_x[2] == '_' 103 | self.top_free = self.pack_y[0] == '_' 104 | self.center_y_free = self.pack_y[1] == '_' 105 | self.bottom_free = self.pack_y[2] == '_' 106 | 107 | self.count_x = count_x 108 | self.count_y = count_y 109 | 110 | self.gap = gap 111 | 112 | enable(self) 113 | 114 | def dimensions(self, count): 115 | if self.height == 0: 116 | return 1, count 117 | ratio = self.width/self.height 118 | count_x = math.sqrt(count * self.width/self.height) 119 | count_y = math.sqrt(count * self.height/self.width) 120 | operations = ( 121 | (math.floor, math.floor), 122 | (math.floor, math.ceil), 123 | (math.ceil, math.floor), 124 | (math.ceil, math.ceil) 125 | ) 126 | best = None 127 | best_x = None 128 | best_y = None 129 | for oper in operations: 130 | cand_x = oper[0](count_x) 131 | cand_y = oper[1](count_y) 132 | diff = cand_x*cand_y - count 133 | if diff >= 0: 134 | if best is None or diff < best: 135 | best = diff 136 | best_x = cand_x 137 | best_y = cand_y 138 | return (best_x, best_y) 139 | 140 | def layout(self): 141 | count = len(self.subviews) 142 | if count == 0: return 143 | 144 | count_x, count_y = self.count_x, self.count_y 145 | if count_x is None and count_y is None: 146 | count_x, count_y = self.dimensions(count) 147 | elif count_x is None: 148 | count_x = math.ceil(count/count_y) 149 | elif count_y is None: 150 | count_y = math.ceil(count/count_x) 151 | if count > count_x * count_y: 152 | raise ValueError( 153 | f'Fixed counts (x: {count_x}, y: {count_y}) not enough to display all views') 154 | 155 | borders = 2 * self.border_width 156 | 157 | dim_x = (self.width-borders-(count_x+1)*self.gap)/count_x 158 | dim_y = (self.height-borders-(count_y+1)*self.gap)/count_y 159 | 160 | dim = min(dim_x, dim_y) 161 | 162 | px = self.pack_x 163 | exp_pack_x = px[0] + px[1]*(count_x-1) + px[2] 164 | py = self.pack_y 165 | exp_pack_y = py[0] + py[1]*(count_y-1) + py[2] 166 | free_count_x = exp_pack_x.count('_') 167 | free_count_y = exp_pack_y.count('_') 168 | 169 | if free_count_x > 0: 170 | per_free_x = ( 171 | self.width - 172 | borders - 173 | count_x*dim - 174 | (count_x+1-free_count_x)*self.gap)/free_count_x 175 | if free_count_y > 0: 176 | per_free_y = ( 177 | self.height - 178 | borders - 179 | count_y*dim - 180 | (count_y+1-free_count_y)*self.gap)/free_count_y 181 | 182 | real_dim_x = dim_x if free_count_x == 0 else dim 183 | real_dim_y = dim_y if free_count_y == 0 else dim 184 | 185 | subviews = iter(self.subviews) 186 | y = self.border_width + (per_free_y if self.top_free else self.gap) 187 | for row in range(count_y): 188 | x = self.border_width + (per_free_x if self.leading_free else self.gap) 189 | for col in range(count_x): 190 | try: 191 | view = next(subviews) 192 | except StopIteration: 193 | break 194 | view.frame = (x, y, real_dim_x, real_dim_y) 195 | x += real_dim_x + (per_free_x if self.center_x_free else self.gap) 196 | y += real_dim_y + (per_free_y if self.center_y_free else self.gap) 197 | 198 | 199 | class Views(dict): 200 | ''' A class that is used to create a hierarchy of ui views defined by 201 | a tree structure, and with the given constraints. 202 | Also stores the created views in depth-first order. 203 | Views can be accessed equivalently with dict references or as attributes: 204 | 205 | * `views['top']` 206 | * `views.top` 207 | ''' 208 | 209 | def __init__(self): 210 | super().__init__() 211 | self._create_views() 212 | 213 | def view_hierarchy(self): 214 | ''' Sample view hierarchy dictionary: 215 | 216 | { 'root': (ui.View, { 217 | 'top': (ui.View, { 218 | 'search_text': ui.TextField, 219 | 'search_action': ui.Button, 220 | }), 221 | 'middle': ui.View, 222 | 'bottom': (ui.View, { 223 | 'accept': ui.Button, 224 | 'cancel': ui.Button, 225 | }) 226 | }) } 227 | 228 | I.e. view names as keys, view classes as values. 229 | If the value is a tuple instead, the first value must be the view class 230 | and the second value a dictionary for the next level of the view 231 | hierarchy. 232 | 233 | View names must match the requirements for identifiers, and 234 | not be any of the Python keywords or attributes of this class 235 | (inheriting `dict`). ''' 236 | 237 | return ( 'root', ui.View ) 238 | 239 | def view_defaults(self, view): 240 | ''' Views are initialized with no arguments. This method is called 241 | with the initialized view to set any defaults you want. 242 | The base implementation creates black views with 243 | white borders, tint and text. ''' 244 | bg = 'black' 245 | fg = 'white' 246 | view.background_color = bg 247 | view.border_color = fg 248 | view.border_width = 1 249 | view.tint_color = fg 250 | view.text_color = fg 251 | 252 | def set_constraints(self): 253 | ''' After all views have been initialized and included in 254 | the hierarchy, this method is called to set the constraints. 255 | Base implementation does nothing. ''' 256 | pass 257 | 258 | def present(self): 259 | ''' Presents the root view of the hierarchy. The base implementation 260 | is a plain `present()` with no arguments. 261 | Return `self` so that you can combine the call with hierarchy init: 262 | 263 | views = Views().present() 264 | ''' 265 | next(iter(self.values())).present() 266 | return self 267 | 268 | def __getattr__(self, key, oga=object.__getattribute__): 269 | if key in self: 270 | return self[key] 271 | else: 272 | return oga(self, key) 273 | 274 | def _create_views(self): 275 | ''' Method that creates a view hierarchy as specified by the 276 | view hierarchy spec. 277 | Each created view is stored by name in `self`. 278 | ''' 279 | 280 | def recursive_view_generation(view_spec, parent): 281 | if parent is None: 282 | assert len(view_spec) in (2, 3), 'Give exactly one root element' 283 | previous_view = None 284 | for is_subspec, group in groupby(view_spec, lambda x: type(x) is tuple): 285 | if is_subspec: 286 | recursive_view_generation(next(group), previous_view) 287 | continue 288 | for view_name, view_class in chunked(group, 2): 289 | assert ( 290 | view_name.isidentifier() 291 | ), f'{view_name} is not a valid identifier' 292 | assert ( 293 | not keyword.iskeyword(view_name) 294 | ), f'Cannot use a keyword as a view name ({view_name})' 295 | assert ( 296 | not view_name in dir(self) 297 | ), f'{view_name} is a member of Views class' 298 | 299 | previous_view = view = view_class(name=view_name) 300 | if parent: 301 | parent.add_subview(view) 302 | self.view_defaults(view) 303 | self[view_name] = view 304 | if parent is None: 305 | self.set_constraints() 306 | 307 | recursive_view_generation(self.view_hierarchy(), None) 308 | 309 | -------------------------------------------------------------------------------- /dashboard.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | from dashboard_conf import * 4 | from ui import * 5 | from objc_util import * 6 | from anchor import * 7 | import math, sys, json, requests, time 8 | from urllib.parse import urlsplit 9 | from functools import partial 10 | from types import SimpleNamespace as NS 11 | 12 | from genr import genr 13 | 14 | import carnet 15 | 16 | class Dashboard(ui.View): 17 | 18 | def __init__(self, **kwargs): 19 | super().__init__(**kwargs) 20 | enable(self) 21 | self.active = True 22 | self.session = requests.Session() 23 | self.heating_now = None 24 | 25 | self.grid_view = GridView( 26 | frame=self.bounds, 27 | flex='WH', 28 | pack=GridView.CENTER) 29 | self.add_subview(self.grid_view) 30 | self.grid_view.dock.all() 31 | self.create_cards() 32 | 33 | refresh = self.refresh_indicator = ImageView( 34 | image=Image('iow:ios7_refresh_empty_256'), 35 | hidden=True) 36 | self.add_subview(self.refresh_indicator) 37 | refresh.dock.top_trailing() 38 | refresh.at.width == 20 39 | refresh.at.height == refresh.at.width 40 | self.token = None 41 | self.display_odometer = True 42 | self.odometer_values = None 43 | 44 | heating = self.heating_request_indicator = ImageView( 45 | image=Image('iow:social_rss_outline_256'), 46 | hidden=True) 47 | self.add_subview(self.heating_request_indicator) 48 | heating.dock.bottom_trailing() 49 | heating.align.width(refresh) 50 | heating.align.height(refresh) 51 | 52 | def create_cards(self): 53 | self.temperature = self.create_card('Lämpötila °C') 54 | self.forecast = self.create_card('Ennuste °C') 55 | self.odometer = self.create_card( 56 | 'Matka km', 57 | action=self.toggle_odometer) 58 | self.range = self.create_card('Kantama KM') 59 | self.power_available = self.create_card('Latausjohto') 60 | self.charge_level = self.create_card('Varaus') 61 | self.doors = self.create_card('Lukitus') 62 | self.heating = self.create_card('Lämmitys', action=self.start_heating_by_click) 63 | 64 | def create_card(self, title, image=False, action=None): 65 | if not action: 66 | card = View() 67 | else: 68 | card = Button() 69 | card.action = action 70 | card.background_color = '#6b95ff' 71 | self.grid_view.add_subview(card) 72 | title_view = Label( 73 | name='title', 74 | text=title.upper(), 75 | text_color='white', 76 | font=('Futura', 10), 77 | number_of_lines=0) 78 | card.add_subview(title_view) 79 | title_view.dock.bottom_trailing() 80 | if image: 81 | content_view = ImageView(name='content', image=Image('iow:more_256'), 82 | hidden=True, 83 | number_of_lines=0, 84 | ) 85 | share=.3 86 | else: 87 | content_view = Label( 88 | name='content', 89 | text='...', 90 | text_color='white', 91 | font=('Futura', 32), 92 | alignment=ALIGN_CENTER, 93 | hidden=True) 94 | share=.9 95 | card.add_subview(content_view) 96 | content_view.align.center_x(card) 97 | content_view.at.center_y == card.at.center_y * 1.25 98 | content_view.at.width == card.at.width * share 99 | ''' 100 | placeholder = ImageView( 101 | name='placeholder', 102 | image=Image('iow:more_256')) 103 | card.add_subview(placeholder) 104 | C(placeholder).dock_center(share=.3) 105 | ''' 106 | return card 107 | 108 | @on_main_thread 109 | def reveal(self, card, text=None, image=None, title=None): 110 | content = card['content'] 111 | if text: 112 | content.text = text 113 | elif image: 114 | content.image = Image(image) 115 | if title: 116 | card['title'].text = title.upper() 117 | content.hidden = False 118 | card.background_color = '#6b95ff' 119 | #card['placeholder'].hidden = True 120 | 121 | icon_map = { 122 | 'clear-day': 'iow:ios7_sunny_outline_256', 123 | 'clear-night': 'iow:ios7_moon_outline_256', 124 | 'rain': 'iow:ios7_rainy_outline_256', 125 | 'snow': 'iow:ios7_rainy_256', 126 | 'sleet': 'iow:ios7_rainy_outline_256', 127 | 'wind': 'iow:ios7_rewind_outline_256', 128 | 'fog': 'iow:drag_256', 129 | 'cloudy': 'iow:ios7_cloud_outline_256', 130 | 'partly-cloudy-day': 'iow:ios7_partlysunny_256', 131 | 'partly-cloudy-night': 'iow:ios7_partlysunny_outline_256' 132 | } 133 | 134 | @genr 135 | def main(self): 136 | self.request_forecast() 137 | self.carnet_login() 138 | yield 139 | while self.active: 140 | self.refresh_indicator.hidden = False 141 | self.get_emanager_status() 142 | self.get_vehicle_status() 143 | carnet.getRemoteAccessHeating(self.session, self.url) 144 | self.refresh_indicator.hidden = True 145 | time.sleep(5) 146 | self.carnet_logout() 147 | 148 | @genr 149 | def request_forecast(self): 150 | try: 151 | key = darksky_conf['api_key'] 152 | latitude = darksky_conf['latitude'] 153 | longitude = darksky_conf['longitude'] 154 | url = f'https://api.darksky.net/forecast/{key}/{latitude},{longitude}?units=si' 155 | result = requests.get(url) 156 | self.show_forecast(result.json()) 157 | except Exception as e: 158 | print('Retrieving forecast') 159 | print(e) 160 | 161 | @on_main_thread 162 | def show_forecast(self, data): 163 | today = data['daily']['data'][0] 164 | low = round(today['temperatureLow']) 165 | high = round(today['temperatureHigh']) 166 | icon_name = today['icon'] 167 | 168 | self.reveal(self.forecast, text=f'{low}/{high}') 169 | 170 | weather_icon = ImageView( 171 | name='icon', image=Image(self.icon_map.get(icon_name, 'iow:ios7_close_outline_256')), 172 | hidden=True) 173 | self.forecast.add_subview(weather_icon) 174 | weather_icon.dock.top_leading(share=.35) 175 | weather_icon.hidden = False 176 | 177 | @genr 178 | def carnet_login(self): 179 | self.url, msg = carnet.CarNetLogin( 180 | self.session, 181 | CARNET_USERNAME, CARNET_PASSWORD) 182 | if self.url == '': 183 | print('Failed to login', msg) 184 | 185 | @in_background 186 | def get_car_data(self): 187 | while self.active: 188 | self.refresh_indicator.hidden = False 189 | self.get_emanager_status() 190 | self.get_vehicle_status() 191 | self.refresh_indicator.hidden = True 192 | time.sleep(5) 193 | 194 | def will_close(self): 195 | self.active = False 196 | 197 | @genr 198 | def carnet_logout(self): 199 | command = '/-/logout/revoke' 200 | r = self.session.post( 201 | self.url + command, 202 | headers=carnet.request_headers) 203 | 204 | def get_emanager_status(self): 205 | command = '/-/emanager/get-emanager' 206 | r = self.session.post( 207 | self.url + command, 208 | headers=carnet.request_headers) 209 | data = r.json() 210 | status_data = data['EManager']['rbc']['status'] 211 | climate_data = data['EManager']['rpc']['status'] 212 | 213 | self.reveal(self.doors, 214 | 'OK' 215 | if status_data['lockState'] == 'LOCKED' 216 | else 'AUKI') 217 | self.reveal(self.charge_level, 218 | str(status_data['batteryPercentage'])+'%') 219 | self.reveal(self.power_available, 220 | 'OK' 221 | if status_data['extPowerSupplyState'] == 'AVAILABLE' 222 | else 'IRTI') 223 | self.reveal(self.range, 224 | str(status_data['electricRange'])) 225 | 226 | self.heating_now = not( 227 | climate_data['climatisationState'] == 'OFF') 228 | self.reveal(self.heating, 229 | 'PÄÄLLÄ' if self.heating_now 230 | else 'EI') 231 | 232 | def get_vehicle_status(self): 233 | command = '/-/vehicle-info/get-vehicle-details' 234 | r = self.session.post( 235 | self.url + command, 236 | headers=carnet.request_headers) 237 | data = r.json() 238 | vehicle_data = data['vehicleDetails'] 239 | 240 | mileage = vehicle_data['distanceCovered'].replace('.', '') 241 | service_components = vehicle_data['serviceInspectionData'].split() 242 | service_in_days = service_components[0] 243 | service_in_km = service_components[3].replace('.', '') 244 | 245 | self.odometer_values = NS( 246 | mileage=mileage, 247 | service_in_km=service_in_km, 248 | service_in_days=service_in_days 249 | ) 250 | 251 | self.reveal( 252 | self.odometer, 253 | f'{mileage}', title='Matka km') 254 | self.display_odometer = True 255 | 256 | @genr 257 | def toggle_heating(self): 258 | self.heating_now = self.heating_now == False 259 | post_data = { 260 | 'triggerAction': self.heating_now, 261 | 'electricClima': True 262 | } 263 | command = '/-/emanager/trigger-climatisation' 264 | r = self.session.post( 265 | self.url + command, post_data, 266 | headers=carnet.request_headers) 267 | print(r.status_code) 268 | print(r.text[:200]) 269 | 270 | ''' 271 | def stop_heating(self): 272 | post_data = { 273 | 'triggerAction': False, 274 | 'electricClima': True 275 | } 276 | command = '/-/emanager/trigger-climatisation' 277 | r = self.session.post( 278 | self.url + command, post_data, 279 | headers=carnet.request_headers) 280 | ''' 281 | 282 | def start_heating_by_click(self, sender): 283 | if self.heating_now is None: 284 | return 285 | self.heating.background_color = 'red' 286 | #self.heating_request_indicator.hidden = False 287 | self.toggle_heating() 288 | #self.heating_request_indicator.hidden = True 289 | 290 | def toggle_odometer(self, sender): 291 | if self.odometer_values is None: return 292 | self.display_odometer = self.display_odometer == False 293 | ov = self.odometer_values 294 | if self.display_odometer: 295 | self.reveal(self.odometer, 296 | f'{ov.mileage}', title='Matka km') 297 | else: 298 | self.reveal(self.odometer, f'{ov.service_in_km}\n{ov.service_in_days}', title='Huolto km, pv') 299 | 300 | def connectivity_issue(self, exception): 301 | pass 302 | 303 | 304 | v = Dashboard(background_color='black') 305 | v.present('full_screen', hide_title_bar=True) 306 | 307 | v.main() 308 | 309 | ''' 310 | with cf.ThreadPoolExecutor() as e: 311 | futures = set() 312 | futures.add(e.submit(v.request_forecast)) 313 | futures.add(e.submit(v.carnet_login)) 314 | for f in cf.as_completed(futures): 315 | f.result() 316 | 317 | v.get_car_data() 318 | ''' 319 | 320 | if len(sys.argv) == 2 and sys.argv[1] == 'warmup': 321 | v.call_soon(v.start_heating()) 322 | #v.start_loop() 323 | 324 | --------------------------------------------------------------------------------