├── 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 |
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 |
240 |
241 |
242 | | A1 | A2 | A3 |
243 |
244 |
245 | | B1 | B2 | B3 |
246 |
247 |
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 |
--------------------------------------------------------------------------------