14. Breathe
This app provides timers for breath training.
14.1. App Theme
14.2. Settings
In General Settings set the App Name to "Breathe" and add a logo that has ot be first added to assets.
In Settings: Dependencies, in order to get the switch and slider components from Anvil extras, add:
dependency: C6ZZPAPN4YYF5NVJ
package name: anvil-extras
The anvil extras components will now be available in the toolbox.
14.3. Assets
The html and css are added to the assets folder on creating the app.
The other assets used in this app are shown below.
They are added by hovering over the three dots button to the right of Assets in the sidebar menu.
The png file is used as a logo.
The mp3 files are short 1 seconds long sound files for the inhale and exhale sounds.
14.4. Forms and modules
Create the forms and modules shown above.
14.5. From1
Form1 is the main form for the app.
The design view of Form1 is shown below.
The components are links with icons set and click event handlers added.
The python code for Form1 is shown below.
The import of the sound module is to load voices for windows users.
1# -------------------------
2# Imports for Form1
3# -------------------------
4from ._anvil_designer import Form1Template # The generated template for Form1
5from anvil import *
6import anvil.server
7
8# Import other forms directly by their module names
9from Sliders import Sliders
10from Settings import Settings
11from Info import Info
12from Sounds import Sounds
13
14 # path ..
15from .. import voice_bootstrap
16
17class Form1(Form1Template):
18 def __init__(self, **properties):
19 # Set Form properties and Data Bindings.
20 self.init_components(**properties)
21 # Ensure a voice menu for windows exists immediately
22 voice_bootstrap.ensure_voice_ready()
23 # Go to Sliders ready to use
24 self.sliders_link_click()
25
26 # -------------------------
27 # Helper methods
28 # -------------------------
29 def reset_links(self):
30 self.info_link.role = ''
31 self.settings_link.role = ''
32 self.sliders_link.role = ''
33 self.sounds_link.role = ''
34
35 def _navigate_to(self, new_form_cls, link=None):
36 """Stop current form, clear panel, and add new form."""
37 # Stop any currently running form first
38 current_components = self.content_panel.get_components()
39 if current_components:
40 current = current_components[0]
41 if hasattr(current, "_hard_stop"):
42 current._hard_stop() # stops speech/audio/animation immediately
43
44 # Clear panel and add new form
45 self.content_panel.clear()
46 self.content_panel.add_component(new_form_cls())
47
48 # Update link role if provided
49 if link:
50 link.role = 'selected'
51
52
53 # -------------------------
54 # Link click methods
55 # -------------------------
56 def info_link_click(self, **event_args):
57 self.reset_links()
58 self._navigate_to(Info, link=self.info_link)
59
60 def settings_link_click(self, **event_args):
61 self.reset_links()
62 self._navigate_to(Settings, link=self.settings_link)
63
64 def sliders_link_click(self, **event_args):
65 self.reset_links()
66 self._navigate_to(Sliders, link=self.sliders_link)
67
68 def sounds_link_click(self, **event_args):
69 self.reset_links()
70 self._navigate_to(Sounds, link=self.sounds_link)
71
72 # Optional title links
73 def Settings_title_link_click(self, **event_args):
74 self.settings_link_click()
75
76 def Info_title_link_click(self, **event_args):
77 self.info_link_click()
78
79 def Sliders_title_link_click(self, **event_args):
80 self.sliders_link_click()
81
82 def Sounds_title_link_click(self, **event_args):
83 self.sounds_link_click()
14.6. Info form
The design view of Info form is shown below.
The markdown text for the info text is shown below.
1**Usage on iphones**
2Use FIrefox to play phase sounds.
3Use FIrefox to continue cycles past 5 min.
4Change voices for speech in the system settings.
5
6=====================================
7
8**Settings (Cog icon)**
9Choose:
10* the number of breathing cycles
11* the pattern of breathing: in, hold, out, hold
12
13**Sound Settings (Notes icon)**
14Choose:
15* the voice
16* the voice rate [slight adjustments may be needed for the timing of counts for different voices on different devices)
17* play sounds on or off
18* speak breathing phase on or off
19* speak counts on or off
20
21**Breathe (Sliders)**
22Click the play icon to start a breathing session.
23
24===============================
254 numbers represent inhale-hold-exhale-hold
263 numbers omit the second hold
272 numbers are inhale-exhale
28
29## **Breathing Patterns**
30
31#### **1) Box / Square Breathing** (Intermediate, Focus)
32
33**Purpose:** Anxiety control, ADHD focus, pre-performance calming, emotional regulation, stress management, tactical breathing, attention training.
34Equal phases → strong cognitive control and rhythm.
35**Patterns:**
363–3–3–3, 4–4–4–4, 5–5–5–5, 6–6–6–6
37
38---
39
40#### **2) Equal Inhale–Exhale (No Holds)** (Beginner, Calm)
41
42**Purpose:** Relaxation, HRV coherence, gentle parasympathetic activation, beginner-friendly breathing.
43Also called coherent breathing (especially 5–5 or 6–6).
44**Patterns:**
453–0–3–0, 4–0–4–0, 5–0–5–0, 6–0–6–0
46
47---
48
49#### **3) Equal Inhale–Exhale with Inhale Hold** (Intermediate, Mindfulness)
50
51**Purpose:** Breath awareness, mindfulness, attention training, mild CO₂ tolerance.
52Short inhale holds add structure without strain.
53**Patterns:**
543–3–3–0, 4–4–4–0, 5–5–5–0, 6–6–6–0
55
56---
57
58#### **4) Equal Inhale–Exhale with Exhale Hold** (Intermediate, Calm)
59
60**Purpose:** Calming, CO₂ tolerance. Exhale hold increases vagal tone and stillness.
61**Patterns:**
623–0–3–3, 4–0–4–4, 5–0–5–5, 6–0–6–6
63
64---
65
66#### **5) Extended Exhale (No Holds)** (Beginner/Intermediate, Relaxation & Sleep)
67
68**Purpose:** Strong relaxation, anxiety reduction, down-regulation, sleep onset.
69Exhale longer than inhale → parasympathetic dominance, effective for acute stress.
70**Patterns:**
712–0–6–0, 3–0–6–0, 4–0–6–0, 4–0–8–0
72
73---
74
75#### **6) Inhale-Hold Emphasis (Structured Relaxation)** (Intermediate, Breath Control)
76
77**Purpose:** Breath control, anxiety exposure, rhythm discipline.
78Short holds add structure without long breath suspensions.
79**Patterns:**
804–2–4–0, 4–2–6–0
81
82---
83
84#### **7) Extended Exhale after Inhale Hold** (Intermediate/Advanced, Calm & Sleep)
85
86**Purpose:** Deep calming, insomnia support, panic reduction.
87Slow exhale after hold strongly activates the parasympathetic system.
88**Patterns:**
894–4–8–0, 4–7–8–0 (Weil breathing)
90
91---
92
93#### **8) Fast Energising Breathing** (Beginner/Intermediate, Energise)
94
95**Purpose:** Increase alertness, counter fatigue, pre-task activation.
96Fast, no-hold breathing raises arousal and focus by increasing respiratory rate. Best used briefly, not for relaxation.
97**Patterns:**
982–0–2–0, 2–0–3–0
99
100---
101
102#### **9) Two-Stage Inhale (Physiological Sigh)** (Intermediate, Rapid Calm)
103
104**Purpose:** Rapid calming, anxiety interruption, nervous system reset.
105Double inhale followed by a long exhale improves lung inflation and strongly activates calming pathways. Most effective in a few cycles.
106**Patterns:**
1072–1–6–0, 3–1–6–0
108
109---
110
111#### **10) Long Exhale-Hold Tolerance** (Advanced, Breath Control & Calm)
112
113**Purpose:** CO₂ tolerance, deep parasympathetic conditioning, breath control.
114Exhale pause trains calm responses to air hunger and deepens regulation. Best for experienced users.
115**Patterns:**
1164–0–4–4, 4–0–6–6, 4–0–6–8
117
118===============================
119
120# **Types of Breathing**
121There are different modes of breathing, each with a slightly different process for inspiration and expiration.
122
123#### **1) Quiet breathing** (Eupnea):
124
125 occurs at rest without thought. The diaphragm and external intercostals contract.
126Normal rates for adults are typically between 12 and 20 breaths per minute.
127
128#### **2) Diaphragmatic breathing** (belly breathing):
129
130the diaphragm contracts as the belly moves out, keeping the chest still. As the diaphragm relaxes, air passively leaves the lungs.
131
132#### **3)Costal breathing** (shallow chest breathing):
133occurs via contraction of the intercostal muscles. As the intercostal muscles relax, air passively leaves the lungs.
134
135#### **4) Hyperpnea** (forced breathing):
136 inspiration and expiration both occur due to contraction of the diaphragm, intercostal muscles, and other accessory muscles. During forced inspiration, muscles of the neck, including the scalenes, contract and lift the thoracic wall, increasing lung volume. During forced expiration, accessory muscles of the abdomen, including the obliques, contract, forcing abdominal organs upward against the diaphragm. This helps to push the diaphragm further into the thorax, pushing more air out. In addition, accessory muscles (primarily the internal intercostals) help to compress the rib cage, which also reduces the volume of the thoracic cavity.
137
138===============================
139
140For **Active Cycle of Breathing Technique ACBT**
141See:
142[https://bronchiectasis.com.au/physiotherapy/techniques/the-active-cycle-of-breathing-technique](https://bronchiectasis.com.au/physiotherapy/techniques/the-active-cycle-of-breathing-technique)
143See:
144[https://www.youtube.com/watch?v=4mXBcA6AI_4](https://www.youtube.com/watch?v=4mXBcA6AI_4)
145[https://www.youtube.com/watch?v=XvorhwGZGm8](https://www.youtube.com/watch?v=XvorhwGZGm8)
146
147For **Autogenic Drainage AD**
148See:
149[https://bronchiectasis.com.au/physiotherapy/techniques/autogenic-drainage](https://bronchiectasis.com.au/physiotherapy/techniques/autogenic-drainage)
150See:
151[https://vimeo.com/148583067](https://vimeo.com/148583067)
14.7. Sounds form
Use the sounds form to set speech voice on windows. On iphones, go so system settings to set voice.
Set the voice rate to suit your device.
Breathing phase sounds can only work on windows devices and Firefox browser on iphone or ipad.
Choose whether to speak the inhale and exhale prompts and the breathing counts.
The design view of Sounds form is shown below.
Use an XY panel to place the labels, dropdown and switches precisely.
The switches have custom css to modify their appearance from the anvil extras default.
The python code for Sounds is shown below.
1from ._anvil_designer import SoundsTemplate
2from anvil import *
3import anvil.js
4import time
5
6from ... import breathing_settings
7
8
9class Sounds(SoundsTemplate):
10 def __init__(self, **properties):
11 self.init_components(**properties)
12 # before voice manager gets voices since rate used for test speaking
13 self.speech_rate = breathing_settings.get_voice_rate()
14 self.set_speech_rate()
15 # initialize voice then update it in voice manager
16 self.selected_voice = None
17 self.voices = []
18 self.voice_manager = Sounds.VoiceManager()
19 self.load_voices()
20 # speech and sounds
21 self.set_play_phase_sounds_bool()
22 self.set_speak_phases_bool()
23 self.set_speak_counts_bool()
24 # so developer can check text in user agent
25 self.ua_text.text = self._get_ua_text()
26 # Audio map for testing
27 self._audio_test_map = {
28 "high": "./_/theme/high.mp3",
29 "low": "./_/theme/low.mp3",
30 "hold": "./_/theme/hold.mp3",
31 "silence": "./_/theme/silence.mp3"
32 }
33 # clear errors
34 self.audio_debug_label.text = ""
35 self._apply_platform_voice_rules()
36
37 def _apply_platform_voice_rules(self):
38 if self._is_ios():
39 # Disable voice selection on iOS
40 self.voice_dropdown.enabled = False
41
42 # Optional UX hint
43 self.voice_dropdown.tooltip = (
44 "On iPhone and iPad, the voice follows your system settings."
45 )
46 else:
47 self.voice_dropdown.enabled = True
48
49
50 # -------------------------
51 # settings module speach rate
52 # -------------------------
53
54
55 def _is_ios(self):
56 """
57 Detect iOS devices (iPhone / iPad / iPod).
58 Works across Safari, Chrome, Firefox iOS.
59 """
60 win = anvil.js.window
61 ua = win.navigator.userAgent
62
63 return (
64 "iPhone" in ua
65 or "iPad" in ua
66 or "iPod" in ua
67 # iPadOS 13+ reports as Mac
68 or ("Macintosh" in ua and win.navigator.maxTouchPoints > 1)
69 )
70
71
72 # -------------------------
73 # settings module speach rate
74 # -------------------------
75
76 def set_speech_rate(self):
77 # print(self.speech_rate)
78 self.voice_rate.selected_value = str(self.speech_rate)
79
80 def voice_rate_change(self, **event_args):
81 breathing_settings.set_voice_rate(self.voice_rate.selected_value)
82 self.speech_rate = self.voice_rate.selected_value
83
84 # -------------------------
85 # settings module play_phase_sounds
86 # -------------------------
87
88 def set_play_phase_sounds_bool(self):
89 # checked is property for enabled or not
90 self.play_phase_sounds.checked = breathing_settings.get_play_phase_sounds_bool()
91
92 def play_phase_sounds_change(self, **event_args):
93 play_phase_sounds_bool = self.play_phase_sounds.checked
94 breathing_settings.set_play_phase_sounds_bool(play_phase_sounds_bool)
95
96
97 # -------------------------
98 # settings module speak_phases
99 # -------------------------
100
101 def set_speak_phases_bool(self):
102 # checked is property for enabled or not
103 self.speak_phases.checked = breathing_settings.get_speak_phases_bool()
104
105 def speak_phases_change(self, **event_args):
106 speak_phases_bool = self.speak_phases.checked
107 breathing_settings.set_speak_phases_bool(speak_phases_bool)
108
109
110 # -------------------------
111 # settings module speach rate
112 # -------------------------
113
114 def set_speak_counts_bool(self):
115 # checked is property for enabled or not
116 self.speak_counts.checked = breathing_settings.get_speak_counts_bool()
117
118 def speak_counts_change(self, **event_args):
119 speak_counts_bool = self.speak_counts.checked
120 breathing_settings.set_speak_counts_bool(speak_counts_bool)
121
122 # -------------------------
123 # PLATFORM
124 # -------------------------
125
126 def _get_ua_text(self):
127 win = anvil.js.window
128 ua = win.navigator.userAgent
129 return ua
130
131 # =======================================================
132 # Voice handling for windows
133 # =======================================================
134 def load_voices(self):
135 """
136 Loads voices asynchronously; waits for iOS/Chrome/Firefox to populate. alhtouh vocies do not work on phone
137 Only after voices are loaded can the dropdown and speaking functions be used reliably.
138 """
139 win = anvil.js.window
140 synth = win.speechSynthesis
141
142 def got_voices(voices):
143 # Keep only English voices
144 en_voices = [(v.name, v.lang, v) for v in voices if v.lang.startswith("en")]
145 if not en_voices:
146 # fallback if none available
147 en_voices = [(v.name, v.lang, v) for v in voices]
148 self.voices = en_voices
149
150 if self._is_ios():
151 # Populate dropdown
152 self.voice_dropdown.items = ["default for system"]
153 else:
154 # Populate dropdown
155 self.voice_dropdown.items = [(f"{name} - {lang}", obj) for name, lang, obj in en_voices]
156
157 # Restore stored voice or fallback
158 stored_voice = breathing_settings.get_voice()
159 if stored_voice in [v[2] for v in en_voices]:
160 self.selected_voice = stored_voice
161 else:
162 self.selected_voice = en_voices[0][2]
163 breathing_settings.set_voice(self.selected_voice)
164
165 # Enable test button now that voices are ready
166 self.speak_test.enabled = True
167
168 # iOS may not have voices immediately
169 voices_ready = synth.getVoices()
170 if voices_ready and len(voices_ready) > 0:
171 got_voices(voices_ready)
172 else:
173 # Listen for voiceschanged event
174 def on_voices_changed(event):
175 synth.removeEventListener("voiceschanged", on_voices_changed)
176 got_voices(synth.getVoices())
177
178 synth.addEventListener("voiceschanged", on_voices_changed)
179
180
181 # ------------------------------------------------------
182 # ------------------------------------------------------
183
184 def voices_dropdown_change(self, **event_args):
185 self.selected_voice = self.voice_dropdown.selected_value
186 breathing_settings.set_voice(self.selected_voice)
187 # self._speak_sample()
188
189 def speak_test_click(self, **event_args):
190 self._speak_sample()
191
192 def _speak_sample(self):
193 """
194 Speak a test phrase
195 Using selected voice for desktop, or system voice on iphone
196 """
197 # if not self.selected_voice:
198 # return
199 win = anvil.js.window
200 synth = win.speechSynthesis
201 synth.cancel() # stop any previous speech
202 utter = win.SpeechSynthesisUtterance()
203 utter.text = "Inhale, hold, exhale, 3, 2, 1"
204 # voice has no effect on iphone
205 utter.voice = self.selected_voice
206 utter.rate = float(self.speech_rate)
207 utter.pitch = 1.0
208 synth.speak(utter)
209
210
211 class VoiceManager:
212 """Handles async loading of SpeechSynthesis voices."""
213 def __init__(self):
214 self.voices = None
215 self._callbacks = []
216 self._initialized = False
217
218 def _load_voices(self):
219 if self._initialized:
220 return
221 self._initialized = True
222
223 win = anvil.js.window
224 synth = win.speechSynthesis
225
226 def deliver():
227 # List of (name, lang, object)
228 self.voices = [(v.name, v.lang, v) for v in synth.getVoices()]
229 for cb in self._callbacks:
230 cb(self.voices)
231 self._callbacks = []
232
233 # Already available
234 if synth.getVoices() and len(synth.getVoices()) > 0:
235 deliver()
236 return
237
238 # Otherwise wait for voiceschanged
239 def on_voices_changed(event):
240 synth.removeEventListener("voiceschanged", on_voices_changed)
241 deliver()
242
243 synth.addEventListener("voiceschanged", on_voices_changed)
244
245 def get_voices(self, callback=None):
246 self._load_voices()
247
248 if self.voices is not None:
249 if callback:
250 callback(self.voices)
251 return self.voices
252
253 if callback:
254 self._callbacks.append(callback)
255 return None
256
257
258 def _log_audio_error(self, msg):
259 """Log errors to console and optionally to a label"""
260 print(msg)
261 if hasattr(self, "audio_debug_label"):
262 self.audio_debug_label.text += msg + "\n"
263
264 # -------------------------
265 # SOUNDS TEST BUTTON
266 # -------------------------
267 def sounds_test_click(self, **event_args):
268 """Test inhale, exhale, hold, and silence sounds sequentially."""
269 win = anvil.js.window
270 # Map of sounds; use the correct file paths
271 sounds = {
272 "inhale": "./_/theme/high.mp3",
273 "exhale": "./_/theme/low.mp3",
274 "hold": "./_/theme/hold.mp3",
275 "silence": "./_/theme/silence.mp3"
276 }
277
278 # Clear debug label
279 if hasattr(self, "audio_debug_label"):
280 self.audio_debug_label.text = ""
281
282 # Recursive function to play the next sound
283 def play_next(keys, index=0):
284 if index >= len(keys):
285 return # all done
286
287 key = keys[index]
288 src = sounds[key]
289
290 audio = win.Audio(src)
291 audio.volume = 1
292 audio.currentTime = 0
293
294 # Log errors
295 audio.onerror = lambda evt=None: self._log_audio_error(f"Failed to play {key}")
296
297 # When the sound ends, play the next one
298 audio.onended = lambda evt=None: play_next(keys, index + 1)
299
300 try:
301 p = audio.play()
302 if p:
303 p.catch(lambda e: self._log_audio_error(f"Play blocked ({key}): {e}"))
304 except Exception as e:
305 self._log_audio_error(f"Exception ({key}): {e}")
306
307 # Start the sequence
308 play_next(list(sounds.keys()))
14.8. Settings form
Use the settings form to set the number of breathing cycles and the durations of the inhale, hold and exhale phases.
The design view of Settings form is shown below.
Use an XY panel to place the labels, dropdown and buttons precisely.
The buttons and the dropdown have click event handlers as shown in the python below.
The buttons have custom roles and css to modify their appearance.
The python code for Settings is shown below.
1from ._anvil_designer import SettingsTemplate
2from anvil import *
3
4
5from ... import breathing_settings
6
7class Settings(SettingsTemplate):
8 def __init__(self, **properties):
9 self.init_components(**properties)
10 self.set_cycles()
11
12 def set_cycles(self):
13 self.cycles.selected_value = str(breathing_settings.get_cycles())
14
15 def cycles_change(self, **event_args):
16 cycles_val = int(self.cycles.selected_value)
17 breathing_settings.set_cycles(cycles_val)
18
19 # =================================
20 # 3
21 # =================================
22
23 def relax_3_3_click(self, **event_args):
24 breathing_settings.set_breath_pattern(3, 0, 3, 0)
25
26 def relax_3_0_3_3_click(self, **event_args):
27 breathing_settings.set_breath_pattern(3, 0, 3, 3)
28
29 def relax_3_3_3_click(self, **event_args):
30 breathing_settings.set_breath_pattern(3, 3, 3, 0)
31
32 def box3_click(self, **event_args):
33 breathing_settings.set_breath_pattern(3, 3, 3, 3)
34
35
36 # =================================
37 # 4
38 # =================================
39
40 def relax_4_4_click(self, **event_args):
41 breathing_settings.set_breath_pattern(4, 0, 4, 0)
42
43 def relax_4_0_4_4_click(self, **event_args):
44 breathing_settings.set_breath_pattern(4, 0, 4, 4)
45
46 def relax_4_4_4_click(self, **event_args):
47 breathing_settings.set_breath_pattern(4, 4, 4, 0)
48
49 def box4_click(self, **event_args):
50 breathing_settings.set_breath_pattern(4, 4, 4, 4)
51
52 # =================================
53 # 5
54 # =================================
55
56 def relax_5_5_click(self, **event_args):
57 breathing_settings.set_breath_pattern(5, 0, 5, 0)
58
59 def relax_5_0_5_5_click(self, **event_args):
60 breathing_settings.set_breath_pattern(5, 0, 5, 5)
61
62 def relax_5_5_5_click(self, **event_args):
63 breathing_settings.set_breath_pattern(5, 5, 5, 0)
64
65 def box5_click(self, **event_args):
66 breathing_settings.set_breath_pattern(5, 5, 5, 5)
67
68 # =================================
69 # 6
70 # =================================
71
72 def relax_6_6_click(self, **event_args):
73 breathing_settings.set_breath_pattern(6, 0, 6, 0)
74
75 def relax_6_0_6_6_click(self, **event_args):
76 breathing_settings.set_breath_pattern(6, 0, 6, 6)
77
78 def relax_6_6_6_click(self, **event_args):
79 breathing_settings.set_breath_pattern(6, 6, 6, 0)
80
81 def box6_click(self, **event_args):
82 breathing_settings.set_breath_pattern(6, 6, 6, 6)
83
84 # =================================
85
86 def box8_click(self, **event_args):
87 breathing_settings.set_breath_pattern(8, 8, 8, 8)
88
89 def box10_click(self, **event_args):
90 breathing_settings.set_breath_pattern(10, 10, 10, 10)
91
92 # =================================
93
94 def energize_2_2_click(self, **event_args):
95 breathing_settings.set_breath_pattern(2, 0, 2, 0)
96
97 def energize_2_3_click(self, **event_args):
98 breathing_settings.set_breath_pattern(2, 0, 3, 0)
99
100 def relax_2_0_2_2_click(self, **event_args):
101 breathing_settings.set_breath_pattern(2, 0, 2, 2)
102
103 def relax_2_2_2_click(self, **event_args):
104 breathing_settings.set_breath_pattern(2, 2, 2, 0)
105
106 def box2_click(self, **event_args):
107 breathing_settings.set_breath_pattern(2, 2, 2, 2)
108
109 # =================================
110
111 def relax_2_6_click(self, **event_args):
112 breathing_settings.set_breath_pattern(2, 0, 6, 0)
113
114 def relax_3_6_click(self, **event_args):
115 breathing_settings.set_breath_pattern(3, 0, 6, 0)
116
117 def relax_4_6_click(self, **event_args):
118 breathing_settings.set_breath_pattern(4, 0, 6, 0)
119
120 def relax_4_8_click(self, **event_args):
121 breathing_settings.set_breath_pattern(4, 0, 8, 0)
122
123
124 def relax_4_2_4_click(self, **event_args):
125 breathing_settings.set_breath_pattern(4, 2, 4, 0)
126
127 def relax_4_2_6_click(self, **event_args):
128 breathing_settings.set_breath_pattern(4, 2, 6, 0)
129
130 def relax_4_2_8_click(self, **event_args):
131 breathing_settings.set_breath_pattern(4, 2, 8, 0)
132
133 def relax_4_2_4_2_click(self, **event_args):
134 breathing_settings.set_breath_pattern(4, 2, 4, 2)
135
136 def relax_4_2_6_2_click(self, **event_args):
137 breathing_settings.set_breath_pattern(4, 2, 6, 2)
138
139 def relax_4_2_8_2_click(self, **event_args):
140 breathing_settings.set_breath_pattern(4, 2, 8, 2)
141
142
143 def relax_4_4_8_click(self, **event_args):
144 breathing_settings.set_breath_pattern(4, 4, 8, 0)
145
146 def relax_4_7_8_click(self, **event_args):
147 breathing_settings.set_breath_pattern(4, 7, 8, 0)
148
149 def relax_4_0_6_6_click(self, **event_args):
150 breathing_settings.set_breath_pattern(4, 0, 6, 6)
151
152 def relax_4_0_6_8_click(self, **event_args):
153 breathing_settings.set_breath_pattern(4, 0, 6, 8)
154
14.9. Sliders form
Use the sliders form to set the manually adjust the the durations of the inhale, hold and exhale phases.
Use the sliders form to start, pause and stop the breathing slider animations.
The design view of Sliders form is shown below.
Use an XY panel to place the labels, dropdown and buttons precisely.
The buttons have custom roles and css to modify their appearance.
The python code for Sliders is shown below.
1
2# ============================================================
3# AUDIO PLAYBACK DESIGN (IMPORTANT – READ BEFORE CHANGES)
4# ============================================================
5#
6# This app intentionally uses a "fire-and-forget" audio model
7# for short MP3 cues, combined with SpeechSynthesis for spoken
8# guidance. The design reflects REAL, tested limitations of
9# iOS WebKit-based browsers.
10#
11# ------------------------------------------------------------
12# CRITICAL PLATFORM REALITY (iOS)
13# ------------------------------------------------------------
14#
15# On iOS Safari and iOS Chrome:
16#
17# 1. SpeechSynthesis and HTMLAudioElement (MP3) are
18# MUTUALLY EXCLUSIVE per page session.
19#
20# - If SpeechSynthesis is used first:
21# * Speech works reliably
22# * ALL subsequent MP3 playback FAILS silently
23#
24# - If an MP3 is played first:
25# * The first MP3 may play
26# * SpeechSynthesis NEVER works
27# * Further MP3 playback is unreliable or blocked
28#
29# There is NO recovery, unlock, delay, or workaround.
30#
31# 2. HTMLAudioElement cannot play a SEQUENCE of MP3s
32# unless EACH play is directly user-gesture driven.
33#
34# - First MP3 triggered by a click: works
35# - Timer- or loop-driven MP3 playback: blocked
36#
37# This applies even if SpeechSynthesis is disabled.
38#
39# ------------------------------------------------------------
40# iOS FIREFOX NOTE
41# ------------------------------------------------------------
42#
43# Firefox on iOS uses WebKit but exhibits DIFFERENT,
44# less restrictive behaviour for short MP3 playback.
45#
46# • Sequential MP3 playback may work when Speech is OFF
47# • Behaviour is not guaranteed and may change
48# • Firefox is therefore treated as a "best-effort" case
49#
50# The app intentionally does NOT rely on Firefox-specific
51# behaviour for correctness.
52#
53# On iOS, Firefox’s container is unusually permissive: it can keep a web page’s JavaScript and media playing for 30+ minutes without going to sleep, as long as there’s some audio activity (even small MP3s).
54# Key points
55# Safari/Chrome/WebKit tabs: suspend after ~5 minutes of inactivity, no way around it.
56# Firefox for iOS: sandboxed container allows much longer runtime, observed 30+ min with audio playback.
57# Reason: Firefox’s container manages the page lifecycle differently; it delays iOS’s suspension of inactive tabs while media is playing.
58# No other browsers currently match this behavior on iOS; all others are essentially Safari under the hood.
59# Caveat: This isn’t guaranteed — low battery, screen lock, or iOS updates could still pause the page.
60# Firefox is the only practical iOS browser where long breathing cycles (20–30+ min) can run reliably, and MP3 playback helps maintain the active state.
61
62# ------------------------------------------------------------
63# DESIGN CONSEQUENCES (INTENTIONAL)
64# ------------------------------------------------------------
65#
66# • SpeechSynthesis is the ONLY reliable automated audio
67# system on iOS for timed / looping behaviour.
68#
69# • MP3 phase sounds are therefore:
70# - ENABLED on desktop browsers
71# - ENABLED on Android
72# - ENABLED on iPhone Firefox (when Speech is OFF)
73# - DISABLED on iOS when Speech is active
74#
75# This behaviour is intentional and unavoidable.
76#
77# ------------------------------------------------------------
78# MP3 PLAYBACK MODEL (WHERE ALLOWED)
79# ------------------------------------------------------------
80#
81# 1. _audio_map stores FILE PATHS ONLY (strings)
82# ------------------------------------------------
83# We do NOT store Audio() objects.
84#
85# Each playback creates a FRESH Audio instance:
86# audio = window.Audio(path)
87# audio.play()
88#
89# Reason:
90# - Reusing Audio objects causes dropped or blocked playback
91# due to overlapping play/pause/end state transitions,
92# especially on iOS WebKit.
93
94#
95# 2. Fire-and-forget playback
96# ------------------------------------------------
97# Audio objects are NOT stored, paused, or reused.
98# They are allowed to finish naturally and be garbage collected.
99#
100# 3. No global audio stop / pause
101# ------------------------------------------------
102# Because Audio instances are short-lived and not tracked,
103# we intentionally DO NOT attempt to stop or pause them.
104#
105# _stop_all_audio() is intentionally empty.
106#
107# This avoids iOS WebKit bugs and timing races.
108#
109# ------------------------------------------------------------
110# SPEECH MODEL
111# ------------------------------------------------------------
112#
113# • SpeechSynthesis is managed independently
114# • speechSynthesis.cancel() is safe and immediate
115# • Speech is the PRIMARY audio channel on iOS
116#
117# ------------------------------------------------------------
118# DO NOT REFACTOR WITHOUT RE-TESTING ON REAL iOS DEVICES
119# ------------------------------------------------------------
120#
121# Desktop browser behaviour is NOT representative of iOS.
122# If audio or speech behaviour changes, test on:
123# - iOS Safari
124# - iOS Chrome
125# - iOS Firefox
126#
127# This design is intentional, tested, and platform-constrained.
128# ============================================================
129
130
131
132from ._anvil_designer import SlidersTemplate
133from anvil import *
134import anvil.server
135import time
136import anvil.js
137
138from ... import breathing_settings
139
140
141def error_handler(err):
142 # Silent error handling for iOS speech quirks
143 return
144
145
146set_default_error_handling(error_handler)
147
148
149class Sliders(SlidersTemplate):
150 # -------------------------
151 # INIT
152 # -------------------------
153 def __init__(self, **properties):
154 self.init_components(**properties)
155 self.timer_sleep = 0.095 # at 0.1 seems to be about 10% too slow
156 self.run = False
157 self.paused = False
158 self.set_sliders_from_settings()
159 self.get_runner_settings()
160 self.set_audio_settings()
161 # Cache for full speech strings (cue + numbers)
162 self._speech_cache = {}
163
164 # -------------------------
165 # SETTINGS
166 # -------------------------
167
168 def set_audio_settings(self):
169 """
170 Store only file paths.
171 We intentionally DO NOT store Audio objects here.
172 Each play creates a fresh Audio() instance to avoid iOS Safari playback bugs.
173 """
174 self._audio_map = {
175 "inhale": "./_/theme/high.mp3",
176 "exhale": "./_/theme/low.mp3",
177 "hold": "./_/theme/hold.mp3",
178 "silence": "./_/theme/silence.mp3",
179 }
180
181 def set_sliders_from_settings(self):
182 # set values
183 self.breath_settings = breathing_settings.get_breath_pattern()
184 self.inhale_slider.value = self.breath_settings["inhale"]
185 self.hold1_slider.value = self.breath_settings["hold1"]
186 self.exhale_slider.value = self.breath_settings["exhale"]
187 self.hold2_slider.value = self.breath_settings["hold2"]
188 # set colours
189 self.slider_color_settings = breathing_settings.get_slider_color_settings()
190 self.inhale_slider.color = self.slider_color_settings["inhale"]
191 self.hold1_slider.color = self.slider_color_settings["hold1"]
192 self.exhale_slider.color = self.slider_color_settings["exhale"]
193 self.hold2_slider.color = self.slider_color_settings["hold2"]
194
195 def get_runner_settings(self):
196 self.cycles_to_go.text = breathing_settings.get_cycles()
197 self.play_phase_sounds = breathing_settings.get_play_phase_sounds_bool()
198 self.speak_phases = breathing_settings.get_speak_phases_bool()
199 self.speak_counts = breathing_settings.get_speak_counts_bool()
200 self.filler = breathing_settings.get_voice_filler()
201
202 # -------------------------
203 # speak text
204 # -------------------------
205
206 def _speak(self, text):
207 if not text:
208 return
209 win = anvil.js.window
210 synth = win.speechSynthesis
211 # synth.cancel() # prevents queue drift (important!)
212 utt = win.SpeechSynthesisUtterance(str(text))
213 voice = breathing_settings.get_voice()
214 if voice:
215 utt.voice = voice
216 utt.rate = breathing_settings.get_voice_rate()
217 synth.speak(utt)
218
219 # -------------------------
220 # play mp3 reliably
221 # -------------------------
222
223 def _play_audio(self, cue_word="silence"):
224 win = anvil.js.window
225 src = self._audio_map.get(cue_word, self._audio_map["silence"])
226
227 # for testing
228 # self._log_audio(f"[AUDIO] Attempt play: {cue_word} -> {src}")
229
230 audio = win.Audio(src)
231 audio.currentTime = 0
232 # audio.volume = 1
233
234 # for testing
235 # audio.onplay = lambda evt=None: self._log_audio(f"[AUDIO] onplay: {cue_word}")
236 # audio.onended = lambda evt=None: self._log_audio(f"[AUDIO] ended: {cue_word}")
237 # audio.onerror = lambda evt=None: self._log_audio(f"[AUDIO] onerror: {cue_word}")
238
239 try:
240 p = audio.play()
241 if p:
242 pass
243 # p.then(lambda _: self._log_audio(f"[AUDIO] play() promise resolved: {cue_word}")) \
244 # .catch(lambda e: self._log_audio(f"[AUDIO] play() promise rejected: {cue_word} -> {e}"))
245 except Exception as e:
246 pass
247 # self._log_audio(f"[AUDIO] Exception calling play(): {cue_word} -> {e}")
248
249
250 # -------------------------
251 # _speech_cache and run phase
252 # -------------------------
253
254 # not yet implemented so cache will grow with changes to patterns etc
255 def refresh_settings_cache(self):
256 self.filler = breathing_settings.get_voice_filler()
257 self.speak_phases = breathing_settings.get_sounds()
258 self.speak_counts = breathing_settings.get_counts()
259 self._speech_cache.clear()
260
261
262 def _get_phase_speech(self, phase, seconds):
263 """
264 Return cached speech string for a phase.
265 - phase: the slider component to animate, 'inhale', 'hold', 'exhale', 'hold', .
266 - seconds: duration of the phase in seconds
267 Only four entries cached for each play, depends on settings:
268 self.speak_phases, self.speak_counts, self.filler
269 # Cache only full phrase strings (not utterances).
270 # We do NOT cache SpeechSynthesisUtterance objects because
271 # voice/rate can change dynamically.
272 """
273 # key depends on settings that affect text
274 # Cache key includes all settings that affect spoken output
275 # so cache remains valid when user toggles options
276 key = (phase, seconds, self.speak_phases, self.speak_counts, self.filler)
277 if key in self._speech_cache:
278 return self._speech_cache[key]
279
280 parts = []
281 if self.speak_phases and phase:
282 parts.append(phase)
283
284 if seconds > 1 and self.speak_counts:
285 if self.speak_phases:
286 numbers = range(int(seconds-1), 0, -1)
287 else:
288 numbers = range(int(seconds), 0, -1)
289 # numbers = range(int(seconds-1), 0, -1)
290 parts.append(self.filler.join(str(i) for i in numbers))
291
292 full_text = " ".join(parts)
293 self._speech_cache[key] = full_text
294 #testing, comment out
295 # print(full_text)
296 return full_text
297
298 # -------------------------
299 # RUN PHASE
300 # -------------------------
301 def _run_phase(self, slider, seconds, slider_name, cue_word):
302 if not self.run or seconds == 0:
303 return
304 # for testing
305 # self._log_audio(f"[PHASE] {cue_word} for {seconds}s")
306
307 # Play audio
308 if self.play_phase_sounds:
309 self._play_audio(cue_word)
310 else:
311 self._play_audio("silence")
312
313 # Speak
314 full_speech = self._get_phase_speech(cue_word, seconds)
315 if full_speech:
316 self._speak(full_speech)
317
318 # Animate slider
319 original_color = slider.color
320 slider.color = breathing_settings.get_slider_animate_color(slider_name)
321 slider.handle_size = 16
322 slider.value = seconds
323
324 ticks = int(seconds * 10)
325 for i in range(ticks, -1, -1):
326 if not self.run:
327 break
328 while self.paused and self.run:
329 time.sleep(0.05)
330 slider.value = round(i / 10, 1)
331 time.sleep(self.timer_sleep)
332
333 slider.color = original_color
334 slider.handle_size = 32
335 slider.value = seconds
336
337 # -------------------------
338 # BREATH PHASES
339 # -------------------------
340 def inhale(self, val=4):
341 self._run_phase(self.inhale_slider, val, "inhale", "inhale")
342
343 def hold1(self, val=0):
344 self._run_phase(self.hold1_slider, val, "hold1", "hold")
345
346 def exhale(self, val=4):
347 self._run_phase(self.exhale_slider, val, "exhale", "exhale")
348
349 def hold2(self, val=0):
350 self._run_phase(self.hold2_slider, val, "hold2", "hold")
351
352 # -------------------------
353 # CONTROL BUTTONS
354 # -------------------------
355
356 def start_breathing_click(self, **event_args):
357 # for testing
358 # self._log_audio_clear()
359 self.run = True
360 # Snapshot current slider values and number of cycles
361 inhale_val = self.inhale_slider.value
362 hold1_val = self.hold1_slider.value
363 exhale_val = self.exhale_slider.value
364 hold2_val = self.hold2_slider.value
365 reps = int(self.cycles_to_go.text)
366
367 # Speak "Get ready" at the start
368 self._speak("Get ready.")
369 time.sleep(3)
370
371 # Main breathing loop
372 for i in range(reps):
373 if not self.run:
374 break
375 self.inhale(inhale_val) # inhale phase with audio + slider
376 self.hold1(hold1_val) # first hold
377 self.exhale(exhale_val) # exhale
378 self.hold2(hold2_val) # second hold
379 # Update cycles remaining in UI
380 self.cycles_to_go.text = reps - (i + 1)
381
382 # Reset cycles display at end
383 self.cycles_to_go.text = breathing_settings.get_cycles()
384
385
386
387 # ------------------------------------------------------
388 # _log_audio for testing
389 # ------------------------------------------------------
390
391 def _log_audio_clear(self):
392 if hasattr(self, "audio_debug_label"):
393 self.audio_debug_label.text = ""
394
395 def _log_audio(self, msg):
396 print(msg)
397 if hasattr(self, "audio_debug_label"):
398 self.audio_debug_label.text += msg + "\n"
399
400 def _log_audio_error(self, msg):
401 print(msg)
402 if hasattr(self, "audio_debug_label"):
403 self.audio_debug_label.text += msg + "\n"
404
405 # ------------------------------------------------------
406 # pause and stop
407 # ------------------------------------------------------
408
409 def pause_toggle_click(self, **event_args):
410 if not self.run:
411 return
412 if self.paused:
413 self._resume_all()
414 else:
415 self._pause_all()
416
417 def _pause_all(self):
418 self.paused = True
419 # Stop speech immediately
420 self._stop_speech()
421 # Stop all audio immediately
422 # self._stop_all_audio()
423
424 def _resume_all(self):
425 self.paused = False
426
427 # def _pause_all_audio(self):
428 # # # LEGACY — not used.
429 # # _audio_map stores file paths, not Audio objects.
430 # # Pausing audio is intentionally unsupported.
431 # for audio in self._audio_map.values():
432 # audio.pause()
433
434 def stop_click(self, **event_args):
435 self.run = False
436 self.paused = False
437 # Stop speech immediately
438 self._stop_speech()
439 # Stop all audio immediately
440 # self._stop_all_audio()
441 # reset sliders
442 self._reset_all_sliders()
443
444 def _stop_speech(self):
445 win = anvil.js.window
446 synth = win.speechSynthesis
447 synth.cancel() # IMMEDIATE stop + clears queue
448
449 def _stop_all_audio(self):
450 # Let short sounds finish naturally
451 # Intentionally empty.
452 # Audio is played via fresh, short-lived Audio() instances.
453 # We cannot (and should not) stop past instances.
454 # This avoids iOS Safari playback bugs.
455 pass
456
457 def _reset_all_sliders(self):
458 for slider_name in ("inhale", "hold1", "exhale", "hold2"):
459 slider = getattr(self, f"{slider_name}_slider")
460 slider.value = slider.value
461 slider.color = breathing_settings.get_slider_color_settings()[slider_name]
462
463 # ------------------------------------------------------
464 # leave form
465 # ------------------------------------------------------
466
467 def form_hide(self, **event_args):
468 """Called automatically when this form is removed from the page"""
469 self._hard_stop()
470
471 def _hard_stop(self):
472 self.run = False
473 self.paused = False
474 self._stop_speech()
475 # self._stop_all_audio()
476 self._reset_all_sliders()
477
478 # ------------------------------------------------------
479 # sliders
480 # ------------------------------------------------------
481
482
483 def inhale_slider_change(self, handle, **event_args):
484 """This method is called when the slider has finished sliding"""
485 pass
486
487 def inhale_slider_slide(self, handle, **event_args):
488 """This method is called when the slider is sliding or dragging"""
489 pass
490
491 def hold1_slider_change(self, handle, **event_args):
492 """This method is called when the slider has finished sliding"""
493 pass
494
495 def hold1_slider_slide(self, handle, **event_args):
496 """This method is called when the slider is sliding or dragging"""
497 pass
498
499 def exhale_slider_change(self, handle, **event_args):
500 """This method is called when the slider has finished sliding"""
501 pass
502
503 def exhale_slider_slide(self, handle, **event_args):
504 """This method is called when the slider is sliding or dragging"""
505 pass
506
507 def hold2_slider_change(self, handle, **event_args):
508 """This method is called when the slider has finished sliding"""
509 pass
510
511 def hold2_slider_slide(self, handle, **event_args):
512 """This method is called when the slider is sliding or dragging"""
513 pass
514
515
516
517
518
519
520
521
522
14.10. breathing_settings module
Use breathing_settings module to store and retrieve the breathing settings for the app.
The python code for breathing_settings is shown below.
1# This is a module.
2# You can define variables and functions here, and use them from any form.
3# This helps the sliders form get setings from settings form and voices form
4
5# Internal storage for start for set_breath_patterns and settings
6
7import anvil.js
8
9def get_default_ua_speech_rate():
10 win = anvil.js.window
11 ua = win.navigator.userAgent.lower()
12 # 2dp only from 0.20 to 1.40
13 if "ipad" in ua:
14 return "0.65"
15 elif "macintosh" in ua:
16 return "0.65"
17 elif "iphone" in ua:
18 return "0.65"
19 elif "windows" in ua:
20 return "0.70"
21 elif "android" in ua:
22 return "0.35"
23 return "1.00"
24
25# ===============================================================
26
27_settings = {
28 "inhale": 4,
29 "hold1": 4,
30 "exhale": 4,
31 "hold2": 4,
32 "cycles": 6,
33 "play_phase_sounds": False,
34 "speak_phases": True,
35 "speak_counts": True,
36}
37
38voice_rate = get_default_ua_speech_rate()
39# print(voice_rate)
40
41_voice_settings = {
42 "voice": None,
43 "voice_rate": voice_rate,
44 "voice_filler": " , ",
45}
46
47# not used yet
48_slider_theme_colors_settings = {
49 "energy": '#F39C12',
50 "calm": '#57b0eb',
51 "sleep": '#ae2be4',
52 "focus": '#2fcc35',
53}
54
55_slider_color_settings = {
56 "inhale": '#33cc33',
57 "hold1": '#cccccc',
58 "exhale": '#3399ff',
59 "hold2": '#cccccc',
60}
61
62_slider_animate_color_settings = {
63 "inhale": '#99ff99',
64 "hold1": '#e5e5e5',
65 "exhale": '#cce6ff',
66 "hold2": '#e5e5e5',
67}
68
69# -------------------------
70# _slider_color_settings
71# -------------------------
72
73def get_slider_color_settings():
74 return _slider_color_settings
75
76def get_slider_color(phase):
77 return _slider_color_settings[phase]
78
79def get_slider_animate_color_settings():
80 return _slider_animate_color_settings
81
82def get_slider_animate_color(phase):
83 return _slider_animate_color_settings[phase]
84
85# -------------------------
86# Breath pattern
87# -------------------------
88
89def set_breath_pattern(inhale=4, hold1=4, exhale=4, hold2=4):
90 _settings.update({
91 "inhale": inhale,
92 "hold1": hold1,
93 "exhale": exhale,
94 "hold2": hold2
95 })
96
97def get_breath_pattern():
98 # RETURN DICTIONARY
99 return {
100 "inhale": _settings["inhale"],
101 "hold1": _settings["hold1"],
102 "exhale": _settings["exhale"],
103 "hold2": _settings["hold2"]
104 }
105
106# -------------------------
107# Cycles
108# -------------------------
109def set_cycles(cycles):
110 _settings["cycles"] = cycles
111
112def get_cycles():
113 return _settings["cycles"]
114
115# -------------------------
116# play_phase_sounds
117# -------------------------
118
119def set_play_phase_sounds_bool(bool):
120 _settings["play_phase_sounds"] = bool
121
122def get_play_phase_sounds_bool():
123 return _settings["play_phase_sounds"]
124
125# -------------------------
126# speak_phases
127# -------------------------
128
129def set_speak_phases_bool(bool):
130 _settings["speak_phases"] = bool
131
132def get_speak_phases_bool():
133 return _settings["speak_phases"]
134
135# -------------------------
136# Counts
137# -------------------------
138
139def set_speak_counts_bool(bool):
140 _settings["speak_counts"] = bool
141 # print(bool)
142
143def get_speak_counts_bool():
144 return _settings["speak_counts"]
145
146# -------------------------
147# Voice
148# -------------------------
149def set_voice(value):
150 _voice_settings["voice"] = value
151
152def get_voice():
153 return _voice_settings["voice"]
154
155# -------------------------
156# Voice rate
157# -------------------------
158def set_voice_rate(value):
159 _voice_settings["voice_rate"] = value
160
161def get_voice_rate():
162 return _voice_settings["voice_rate"]
163
164# -------------------------
165# Voice filler
166# -------------------------
167def set_voice_filler(self):
168 # " , " is about it, nothing extra pads speech in silence
169 win = anvil.js.window
170 ua = win.navigator.userAgent.lower()
171 if "ipad" in ua:
172 value = " , "
173 elif "macintosh" in ua:
174 value = " , "
175 elif "iphone" in ua:
176 value = " , "
177 elif "windows" in ua:
178 value = " , "
179 elif "android" in ua:
180 value = " , "
181 else:
182 value = " , "
183 _voice_settings["voice_filler"] = value
184
185
186def get_voice_filler():
187 return _voice_settings["voice_filler"]
14.11. voice_bootstrap module
Use voice_bootstrap module to set the voice and rate for speech.
The python code for voice_bootstrap is shown below.
1# voice_bootstrap.py
2# Loads voices asynchronously
3# Picks the first English voice for windows
4# Stores it in breathing_settings
5# Runs automatically
6
7import anvil.js
8from . import breathing_settings
9
10_initialized = False
11_callbacks = []
12
13def ensure_voice_ready(callback=None):
14 """
15 Ensures a voice is selected and stored.
16 Calls callback(voice) when ready.
17 """
18 global _initialized
19
20 voice = breathing_settings.get_voice()
21 if voice:
22 if callback:
23 callback(voice)
24 return
25
26 if callback:
27 _callbacks.append(callback)
28
29 if _initialized:
30 return
31
32 _initialized = True
33
34 win = anvil.js.window
35 synth = win.speechSynthesis
36
37 def select_default():
38 voices = synth.getVoices()
39 if not voices:
40 return
41
42 # Prefer English
43 en = [v for v in voices if v.lang.lower().startswith("en")]
44 chosen = en[0] if en else voices[0] # 0 for first voice
45
46 breathing_settings.set_voice(chosen)
47
48 for cb in _callbacks:
49 cb(chosen)
50 _callbacks.clear()
51
52 # Voices already loaded
53 if synth.getVoices():
54 select_default()
55 return
56
57 # Wait for async load
58 def on_voices_changed(event):
59 synth.removeEventListener("voiceschanged", on_voices_changed)
60 select_default()
61
62 synth.addEventListener("voiceschanged", on_voices_changed)
63
14.12. Theme: Roles
To customize button colours efficiently add roles via the Theme Editor.
The names of new roles appear in the role dropdowns for components to easily assign them for custom css.
An example is shown below.
14.13. Custom css
Custom css for the roles is added into a theme css file in the assets folder.
Custom css for the sliders and switches from anvil extras is also added here.
1/* -------------------------------
2GMC added slider colour and handle
3---------------------------------*/
4/* Slim, round handle */
5.noUi-horizontal .noUi-handle {
6 width: 10px !important; /* thin */
7 /* height: 26px !important; */
8 right: -5px !important; /* half of width */
9 /* top: -4px !important; */
10
11 border-radius: 999px !important; /* fully round */
12 border: 1px solid #90CAF9 !important;
13
14 background: linear-gradient(
15 to bottom,
16 #E3F2FD,
17 #BBDEFB
18 ) !important;
19
20 box-shadow:
21 0 2px 4px rgba(0,0,0,0.25) !important;
22}
23
24/* -------------------------------
25GMC added switch width
26might only keep the important, but left rest their in case want to modify more features
27---------------------------------*/
28
29.ae-switch label .ae-switch-lever {
30 content: "";
31 display: inline-block;
32 position: relative;
33 width: 56px !important;
34 height: 20px !important;
35 background-color: rgba(0,0,0,0.38);
36 border-radius: 15px;
37 margin-right: 10px;
38 -webkit-transition: background 0.3s ease;
39 transition: background 0.3s ease;
40 vertical-align: middle;
41 margin: 0 16px;
42}
43
44.ae-switch label input[type=checkbox]:checked+.ae-switch-lever:after, .ae-switch label input[type=checkbox]:checked+.ae-switch-lever:before {
45 left: 48px !important;
46}
47
48.ae-switch label .ae-switch-lever:after, .ae-switch label .ae-switch-lever:before {
49 content: "";
50 position: absolute;
51 display: inline-block;
52 width: 30px !important;
53 height: 30px !important;
54 border-radius: 50%;
55 left: 0;
56 top: -5px !important;
57 -webkit-transition: left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
58 transition: left 0.3s ease, background 0.3s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
59 transition: left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease;
60 transition: left 0.3s ease, background 0.3s ease, box-shadow 0.1s ease, transform 0.1s ease, -webkit-box-shadow 0.1s ease, -webkit-transform 0.1s ease;
61}
62/* -------------------------------
631) Calm / Relaxation (Blue)
64---------------------------------*/
65.anvil-role-calm-btn > .btn {
66 background-color: #57b0eb;
67 color: #ffffff;
68
69 font-size: 14px;
70 padding: 6px 1px !important;
71 min-height: 0 !important;
72 line-height: 1.2;
73 text-transform: none;
74}
75
76.anvil-role-calm-deep-btn > .btn {
77 background-color: #1b8eda;
78 color: #ffffff;
79
80
81 font-size: 14px;
82 padding: 6px 1px !important;
83 min-height: 0 !important;
84 line-height: 1.2;
85 text-transform: none;
86}
87
88/* -------------------------------
892) Focus / Attention / Control (Green)
90---------------------------------*/
91.anvil-role-focus-btn > .btn {
92 background-color: #2fcc35;
93 color: #ffffff;
94
95 font-size: 14px;
96 padding: 6px 1px !important;
97 min-height: 0 !important;
98 line-height: 1.2;
99 text-transform: none;
100}
101
102.anvil-role-focus-light-btn > .btn {
103 background-color: #56d85b;
104 color: #ffffff;
105
106 font-size: 14px;
107 padding: 6px 1px !important;
108 min-height: 0 !important;
109 line-height: 1.2;
110 text-transform: none;
111}
112
113/* -------------------------------
1143) Sleep / Deep Calm / Therapeutic (Purple)
115---------------------------------*/
116.anvil-role-sleep-btn > .btn {
117 background-color: #bf58ea;
118 color: #ffffff;
119
120 font-size: 14px;
121 padding: 6px 1px !important;
122 min-height: 0 !important;
123 line-height: 1.2;
124 text-transform: none;
125}
126
127.anvil-role-sleep-therapeutic-btn > .btn {
128 background-color: #ae2be4;
129 color: #ffffff;
130 font-size: 14px;
131 padding: 6px 1px !important;
132 min-height: 0 !important;
133 line-height: 1.2;
134 text-transform: none;
135}
136
137/* -------------------------------
1384) Energise / Rapid Alertness (Bright Orange)
139---------------------------------*/
140.anvil-role-energise-btn > .btn {
141 background-color: #F39C12; /* bright orange */
142 color: #ffffff; /* white text */
143
144 font-size: 14px;
145 padding: 6px 1px !important;
146 min-height: 0 !important;
147 line-height: 1.2;
148 text-transform: none;
149}
150
151.anvil-role-calm-btn > .btn:hover,
152.anvil-role-calm-deep-btn > .btn:hover,
153.anvil-role-focus-btn > .btn:hover,
154.anvil-role-focus-light-btn > .btn:hover,
155.anvil-role-sleep-btn > .btn:hover,
156.anvil-role-sleep-therapeutic-btn > .btn:hover,
157.anvil-role-energise-btn > .btn:hover {
158 filter: brightness(1.35);
159 color: #0000ff;
160}
161
162
163
164/*
165see https://anvil.works/articles/using-css#applying-css-via-python
166adding styles for btn role in title bar
167*/
168
169.anvil-role-title-btn .btn {
170 padding: 0px 0px;
171 margins: 0px 0px;
172}