lights-off 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. lights_off-0.1.0/.gitignore +13 -0
  2. lights_off-0.1.0/LICENSE +21 -0
  3. lights_off-0.1.0/PKG-INFO +99 -0
  4. lights_off-0.1.0/README.md +64 -0
  5. lights_off-0.1.0/lights_off/GUI/account_options.py +102 -0
  6. lights_off-0.1.0/lights_off/GUI/accounts.py +66 -0
  7. lights_off-0.1.0/lights_off/GUI/ask.py +10 -0
  8. lights_off-0.1.0/lights_off/GUI/chooser.py +93 -0
  9. lights_off-0.1.0/lights_off/GUI/invisible.py +157 -0
  10. lights_off-0.1.0/lights_off/GUI/lists.py +173 -0
  11. lights_off-0.1.0/lights_off/GUI/main.py +682 -0
  12. lights_off-0.1.0/lights_off/GUI/misc.py +345 -0
  13. lights_off-0.1.0/lights_off/GUI/options.py +206 -0
  14. lights_off-0.1.0/lights_off/GUI/poll.py +40 -0
  15. lights_off-0.1.0/lights_off/GUI/profile.py +57 -0
  16. lights_off-0.1.0/lights_off/GUI/search.py +41 -0
  17. lights_off-0.1.0/lights_off/GUI/timelines.py +46 -0
  18. lights_off-0.1.0/lights_off/GUI/tray.py +42 -0
  19. lights_off-0.1.0/lights_off/GUI/tweet.py +283 -0
  20. lights_off-0.1.0/lights_off/GUI/view.py +365 -0
  21. lights_off-0.1.0/lights_off/SAAPI64.dll +0 -0
  22. lights_off-0.1.0/lights_off/Tolk.dll +0 -0
  23. lights_off-0.1.0/lights_off/Tolk.py +64 -0
  24. lights_off-0.1.0/lights_off/__init__.py +0 -0
  25. lights_off-0.1.0/lights_off/__main__.py +77 -0
  26. lights_off-0.1.0/lights_off/api_log.py +54 -0
  27. lights_off-0.1.0/lights_off/application.py +4 -0
  28. lights_off-0.1.0/lights_off/globals.py +144 -0
  29. lights_off-0.1.0/lights_off/keyboard_handler/LICENSE +19 -0
  30. lights_off-0.1.0/lights_off/keyboard_handler/__init__.py +4 -0
  31. lights_off-0.1.0/lights_off/keyboard_handler/global_handler.py +9 -0
  32. lights_off-0.1.0/lights_off/keyboard_handler/key_constants.py +126 -0
  33. lights_off-0.1.0/lights_off/keyboard_handler/linux.py +72 -0
  34. lights_off-0.1.0/lights_off/keyboard_handler/main.py +97 -0
  35. lights_off-0.1.0/lights_off/keyboard_handler/osx.py +59 -0
  36. lights_off-0.1.0/lights_off/keyboard_handler/windows.py +42 -0
  37. lights_off-0.1.0/lights_off/keyboard_handler/wx_handler.py +138 -0
  38. lights_off-0.1.0/lights_off/keymac.keymap +10 -0
  39. lights_off-0.1.0/lights_off/keymap.keymap +48 -0
  40. lights_off-0.1.0/lights_off/mastodon_account.py +321 -0
  41. lights_off-0.1.0/lights_off/nvdaControllerClient64.dll +0 -0
  42. lights_off-0.1.0/lights_off/sound.py +84 -0
  43. lights_off-0.1.0/lights_off/sounds/default/boundary.ogg +0 -0
  44. lights_off-0.1.0/lights_off/sounds/default/close.ogg +0 -0
  45. lights_off-0.1.0/lights_off/sounds/default/delete.ogg +0 -0
  46. lights_off-0.1.0/lights_off/sounds/default/error.ogg +0 -0
  47. lights_off-0.1.0/lights_off/sounds/default/follow.ogg +0 -0
  48. lights_off-0.1.0/lights_off/sounds/default/home.ogg +0 -0
  49. lights_off-0.1.0/lights_off/sounds/default/like.ogg +0 -0
  50. lights_off-0.1.0/lights_off/sounds/default/likes.ogg +0 -0
  51. lights_off-0.1.0/lights_off/sounds/default/list.ogg +0 -0
  52. lights_off-0.1.0/lights_off/sounds/default/max_length.ogg +0 -0
  53. lights_off-0.1.0/lights_off/sounds/default/media.ogg +0 -0
  54. lights_off-0.1.0/lights_off/sounds/default/mentions.ogg +0 -0
  55. lights_off-0.1.0/lights_off/sounds/default/messages.ogg +0 -0
  56. lights_off-0.1.0/lights_off/sounds/default/new.ogg +0 -0
  57. lights_off-0.1.0/lights_off/sounds/default/notifications.ogg +0 -0
  58. lights_off-0.1.0/lights_off/sounds/default/open.ogg +0 -0
  59. lights_off-0.1.0/lights_off/sounds/default/ready.ogg +0 -0
  60. lights_off-0.1.0/lights_off/sounds/default/search.ogg +0 -0
  61. lights_off-0.1.0/lights_off/sounds/default/send_boost.ogg +0 -0
  62. lights_off-0.1.0/lights_off/sounds/default/send_message.ogg +0 -0
  63. lights_off-0.1.0/lights_off/sounds/default/send_post.ogg +0 -0
  64. lights_off-0.1.0/lights_off/sounds/default/send_reply.ogg +0 -0
  65. lights_off-0.1.0/lights_off/sounds/default/send_retweet.ogg +0 -0
  66. lights_off-0.1.0/lights_off/sounds/default/send_tweet.ogg +0 -0
  67. lights_off-0.1.0/lights_off/sounds/default/unfollow.ogg +0 -0
  68. lights_off-0.1.0/lights_off/sounds/default/unlike.ogg +0 -0
  69. lights_off-0.1.0/lights_off/sounds/default/user.ogg +0 -0
  70. lights_off-0.1.0/lights_off/sounds/default/volume_changed.ogg +0 -0
  71. lights_off-0.1.0/lights_off/speak.py +25 -0
  72. lights_off-0.1.0/lights_off/streaming.py +48 -0
  73. lights_off-0.1.0/lights_off/timeline.py +371 -0
  74. lights_off-0.1.0/lights_off/utils.py +537 -0
  75. lights_off-0.1.0/pyproject.toml +85 -0
  76. lights_off-0.1.0/tests/conftest.py +64 -0
  77. lights_off-0.1.0/tests/test_mastodon_account.py +467 -0
  78. lights_off-0.1.0/tests/test_mastodon_live.py +190 -0
  79. lights_off-0.1.0/tests/test_mastodon_multi_identity.py +74 -0
  80. lights_off-0.1.0/tests/test_utils.py +943 -0
@@ -0,0 +1,13 @@
1
+ windist
2
+ macdist
3
+ .venv
4
+ __pycache__
5
+ .pytest_cache
6
+ *.log
7
+ *.spec
8
+ *.pyc
9
+ QPlay.exe
10
+ quinter_updater\updater.exe
11
+ .env
12
+ .idea
13
+ /.coverage
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Quin and Melody, Matthew Martin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: lights-off
3
+ Version: 0.1.0
4
+ Summary: An accessible Mastodon client for screen reader users, fork of Quinter
5
+ Project-URL: Homepage, https://pypi.org/project/lights-off/
6
+ Project-URL: Repository, https://github.com/matthewdeanmartin/lights-off
7
+ Project-URL: Bug Tracker, https://github.com/matthewdeanmartin/lights-off/issues
8
+ Author: Quin and Mason
9
+ Author-email: Matthew Martin <matthewdeanmartin@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: accessibility,mastodon,screen-reader,wxpython
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Environment :: MacOS X
15
+ Classifier: Environment :: Win32 (MS Windows)
16
+ Classifier: Intended Audience :: End Users/Desktop
17
+ Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Communications
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: accessible-output2>=0.17; sys_platform == 'darwin'
25
+ Requires-Dist: mastodon-py>=2.2.1
26
+ Requires-Dist: pyperclip>=1.8
27
+ Requires-Dist: sound-lib>=0.8.8; sys_platform != 'linux'
28
+ Requires-Dist: tweak>=1.0.4
29
+ Requires-Dist: wxpython>=4.2
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: python-dotenv>=1; extra == 'dev'
33
+ Requires-Dist: ruff>=0.9; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # lights-off
37
+
38
+ **lights-off** is a lightweight, keyboard-driven, accessible Mastodon client for Windows
39
+ and macOS, built with wxPython. It is a fork of
40
+ [Quinter](https://github.com/QuinterApp/Quinter), a Twitter client, converted for the
41
+ Mastodon network.
42
+
43
+ Every action is reachable by keyboard shortcut. All content is routed through the platform
44
+ screen reader (NVDA/JAWS/SAPI on Windows, VoiceOver on macOS) so blind and low-vision
45
+ users can browse, post, and manage their account without touching the mouse.
46
+
47
+ ## Install
48
+
49
+ ```
50
+ pipx install lights-off
51
+ ```
52
+
53
+ Requires Python 3.12+. See the [installation docs](https://lights-off.readthedocs.io/installation/overview/) for platform-specific steps.
54
+
55
+ ## Run
56
+
57
+ ```
58
+ lights-off
59
+ ```
60
+
61
+ On first launch you will be prompted for your Mastodon instance URL (e.g.
62
+ `https://mastodon.social`) and will complete a one-time OAuth browser flow.
63
+
64
+ ## Documentation
65
+
66
+ Full documentation is at <https://lights-off.readthedocs.io/>.
67
+
68
+ - [Installation](https://lights-off.readthedocs.io/installation/overview/)
69
+ - [Screen reader setup](https://lights-off.readthedocs.io/screen-readers/overview/)
70
+ - [Keyboard reference](https://lights-off.readthedocs.io/getting-started/keyboard-reference/)
71
+ - [Troubleshooting](https://lights-off.readthedocs.io/reference/troubleshooting/)
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ git clone https://github.com/matthewdeanmartin/lights-off
77
+ cd lights-off
78
+ uv sync
79
+ uv run lights-off
80
+ ```
81
+
82
+ Run tests across all supported Python versions:
83
+
84
+ ```
85
+ tox
86
+ ```
87
+
88
+ ## Contributing
89
+
90
+ Pull requests welcome. For large feature additions, open an issue first.
91
+
92
+ ## Licenses
93
+
94
+ - Quinter - no declared license, per Mastodon conversation with Quinn, forking and MIT is okay.
95
+ - SAAPI65.dll - (C) All rights reserved
96
+ - nvdaControllerClient64.dll LGPL
97
+ - GPL-2.0-only. `Tolk.py` is LGPLv3 (copyright Davy Kager).
98
+ - .ogg soundfiles - unknown license
99
+ - keyboard_handler - vendorized, MIT, copyright Christopher Toth
@@ -0,0 +1,64 @@
1
+ # lights-off
2
+
3
+ **lights-off** is a lightweight, keyboard-driven, accessible Mastodon client for Windows
4
+ and macOS, built with wxPython. It is a fork of
5
+ [Quinter](https://github.com/QuinterApp/Quinter), a Twitter client, converted for the
6
+ Mastodon network.
7
+
8
+ Every action is reachable by keyboard shortcut. All content is routed through the platform
9
+ screen reader (NVDA/JAWS/SAPI on Windows, VoiceOver on macOS) so blind and low-vision
10
+ users can browse, post, and manage their account without touching the mouse.
11
+
12
+ ## Install
13
+
14
+ ```
15
+ pipx install lights-off
16
+ ```
17
+
18
+ Requires Python 3.12+. See the [installation docs](https://lights-off.readthedocs.io/installation/overview/) for platform-specific steps.
19
+
20
+ ## Run
21
+
22
+ ```
23
+ lights-off
24
+ ```
25
+
26
+ On first launch you will be prompted for your Mastodon instance URL (e.g.
27
+ `https://mastodon.social`) and will complete a one-time OAuth browser flow.
28
+
29
+ ## Documentation
30
+
31
+ Full documentation is at <https://lights-off.readthedocs.io/>.
32
+
33
+ - [Installation](https://lights-off.readthedocs.io/installation/overview/)
34
+ - [Screen reader setup](https://lights-off.readthedocs.io/screen-readers/overview/)
35
+ - [Keyboard reference](https://lights-off.readthedocs.io/getting-started/keyboard-reference/)
36
+ - [Troubleshooting](https://lights-off.readthedocs.io/reference/troubleshooting/)
37
+
38
+ ## Development
39
+
40
+ ```bash
41
+ git clone https://github.com/matthewdeanmartin/lights-off
42
+ cd lights-off
43
+ uv sync
44
+ uv run lights-off
45
+ ```
46
+
47
+ Run tests across all supported Python versions:
48
+
49
+ ```
50
+ tox
51
+ ```
52
+
53
+ ## Contributing
54
+
55
+ Pull requests welcome. For large feature additions, open an issue first.
56
+
57
+ ## Licenses
58
+
59
+ - Quinter - no declared license, per Mastodon conversation with Quinn, forking and MIT is okay.
60
+ - SAAPI65.dll - (C) All rights reserved
61
+ - nvdaControllerClient64.dll LGPL
62
+ - GPL-2.0-only. `Tolk.py` is LGPLv3 (copyright Davy Kager).
63
+ - .ogg soundfiles - unknown license
64
+ - keyboard_handler - vendorized, MIT, copyright Christopher Toth
@@ -0,0 +1,102 @@
1
+ from sound_lib import stream
2
+ import os
3
+ from lights_off import globals
4
+ import wx
5
+
6
+ class general(wx.Panel, wx.Dialog):
7
+ def __init__(self, account, parent):
8
+ _boundary = globals.confpath+"/sounds/default/boundary.ogg"
9
+ if not os.path.exists(_boundary):
10
+ from importlib.resources import files, as_file
11
+ with as_file(files("lights_off").joinpath("sounds/default/boundary.ogg")) as p:
12
+ _boundary = str(p)
13
+ self.snd = stream.FileStream(file=_boundary)
14
+ self.account=account
15
+ super(general, self).__init__(parent)
16
+ self.main_box = wx.BoxSizer(wx.VERTICAL)
17
+ self.soundpacklist_label=wx.StaticText(self, -1, "Soundpacks")
18
+ self.main_box.Add(self.soundpacklist_label, 0, wx.LEFT|wx.RIGHT|wx.TOP, 10)
19
+ self.soundpackslist = wx.ListBox(self, -1, size=(400,150))
20
+ self.main_box.Add(self.soundpackslist, 1, wx.ALL|wx.EXPAND, 10)
21
+ self.soundpackslist.Bind(wx.EVT_LISTBOX, self.on_soundpacks_list_change)
22
+ dirs = os.listdir(globals.confpath+"/sounds")
23
+ for i in range(0,len(dirs)):
24
+ if not dirs[i].startswith("_") and not dirs[i].startswith(".DS"):
25
+ self.soundpackslist.Insert(dirs[i],self.soundpackslist.GetCount())
26
+ if account.prefs.soundpack==dirs[i]:
27
+ self.soundpackslist.SetSelection(self.soundpackslist.GetCount()-1)
28
+ self.sp=dirs[i]
29
+ try:
30
+ dirs2 = os.listdir("sounds")
31
+ for i in range(0,len(dirs2)):
32
+ if not dirs2[i].startswith("_") and not dirs2[i].startswith(".DS") and dirs2[i] not in dirs:
33
+ self.soundpackslist.Insert(dirs2[i],self.soundpackslist.GetCount())
34
+ if account.prefs.soundpack==dirs2[i]:
35
+ self.soundpackslist.SetSelection(self.soundpackslist.GetCount()-1)
36
+ self.sp=dirs2[i]
37
+ except OSError:
38
+ pass
39
+ if not hasattr(self,"sp"):
40
+ self.sp="default"
41
+ instance_url=getattr(account.prefs,"instance","")
42
+ if instance_url:
43
+ self.instance_label=wx.StaticText(self, -1, "Instance: "+instance_url)
44
+ self.main_box.Add(self.instance_label, 0, wx.ALL, 10)
45
+ self.pan_label = wx.StaticText(self, -1, "Sound pan")
46
+ self.main_box.Add(self.pan_label, 0, wx.LEFT|wx.RIGHT|wx.TOP, 10)
47
+ self.soundpan = wx.Slider(self, -1, int(self.account.prefs.soundpan*50),-50,50,name="Soundpack Pan")
48
+ self.soundpan.Bind(wx.EVT_SLIDER,self.OnPan)
49
+ self.main_box.Add(self.soundpan, 0, wx.ALL|wx.EXPAND, 10)
50
+ self.footer_label = wx.StaticText(self, -1, "Post Footer (Optional)")
51
+ self.main_box.Add(self.footer_label, 0, wx.LEFT|wx.RIGHT|wx.TOP, 10)
52
+ self.footer = wx.TextCtrl(self, -1, "",style=wx.TE_MULTILINE, size=(500,80))
53
+ self.main_box.Add(self.footer, 1, wx.ALL|wx.EXPAND, 10)
54
+ self.footer.AppendText(account.prefs.footer)
55
+ self.footer.SetMaxLength(500)
56
+ self.SetSizer(self.main_box)
57
+
58
+ def OnPan(self,event):
59
+ pan=self.soundpan.GetValue()/50
60
+ self.snd.pan=pan
61
+ self.snd.play()
62
+
63
+ def on_soundpacks_list_change(self, event):
64
+ self.sp=event.GetString()
65
+
66
+ class OptionsGui(wx.Dialog):
67
+ def __init__(self,account):
68
+ self.account=account
69
+ wx.Dialog.__init__(self, None, title="Account Options for "+self.account.me.acct, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
70
+ self.Bind(wx.EVT_CLOSE, self.OnClose)
71
+ self.panel = wx.Panel(self)
72
+ self.main_box = wx.BoxSizer(wx.VERTICAL)
73
+ self.notebook = wx.Notebook(self.panel)
74
+ self.general=general(self.account, self.notebook)
75
+ self.notebook.AddPage(self.general, "General")
76
+ self.general.SetFocus()
77
+ self.main_box.Add(self.notebook, 1, wx.ALL|wx.EXPAND, 10)
78
+ button_row = wx.BoxSizer(wx.HORIZONTAL)
79
+ self.ok = wx.Button(self.panel, wx.ID_OK, "&OK")
80
+ self.ok.SetDefault()
81
+ self.ok.Bind(wx.EVT_BUTTON, self.OnOK)
82
+ button_row.Add(self.ok, 0, wx.ALL, 5)
83
+ self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel")
84
+ self.close.Bind(wx.EVT_BUTTON, self.OnClose)
85
+ button_row.Add(self.close, 0, wx.ALL, 5)
86
+ self.main_box.Add(button_row, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
87
+ self.panel.SetSizer(self.main_box)
88
+ self.main_box.Fit(self.panel)
89
+ self.Fit()
90
+ self.SetMinSize(self.GetSize())
91
+ self.Centre()
92
+
93
+ def OnOK(self, event):
94
+ self.account.prefs.soundpack=self.general.sp
95
+ self.account.prefs.soundpan=self.general.soundpan.GetValue()/50
96
+ self.account.prefs.footer=self.general.footer.GetValue()
97
+ self.general.snd.free()
98
+ self.Destroy()
99
+
100
+ def OnClose(self, event):
101
+ self.general.snd.free()
102
+ self.Destroy()
@@ -0,0 +1,66 @@
1
+ from lights_off import application
2
+ import wx
3
+ from lights_off import globals
4
+ from . import main
5
+
6
+ class AccountsGui(wx.Dialog):
7
+ def __init__(self):
8
+ wx.Dialog.__init__(self, None, title="Accounts", style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
9
+ self.Bind(wx.EVT_CLOSE, self.OnClose)
10
+ self.panel = wx.Panel(self)
11
+ self.main_box = wx.BoxSizer(wx.VERTICAL)
12
+ self.list_label=wx.StaticText(self.panel, -1, label="&Accounts")
13
+ self.main_box.Add(self.list_label, 0, wx.LEFT|wx.RIGHT|wx.TOP, 10)
14
+ self.list=wx.ListBox(self.panel, -1, size=(400,200))
15
+ self.main_box.Add(self.list, 1, wx.ALL|wx.EXPAND, 10)
16
+ self.list.SetFocus()
17
+ self.list.Bind(wx.EVT_LISTBOX, self.on_list_change)
18
+ self.add_items()
19
+ button_row = wx.BoxSizer(wx.HORIZONTAL)
20
+ self.load = wx.Button(self.panel, wx.ID_DEFAULT, "&Switch")
21
+ self.load.SetDefault()
22
+ self.load.Bind(wx.EVT_BUTTON, self.Load)
23
+ button_row.Add(self.load, 0, wx.ALL, 5)
24
+ self.new = wx.Button(self.panel, wx.ID_DEFAULT, "&Add account")
25
+ self.new.Bind(wx.EVT_BUTTON, self.New)
26
+ button_row.Add(self.new, 0, wx.ALL, 5)
27
+ self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel")
28
+ self.close.Bind(wx.EVT_BUTTON, self.OnClose)
29
+ button_row.Add(self.close, 0, wx.ALL, 5)
30
+ self.main_box.Add(button_row, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
31
+ self.panel.SetSizer(self.main_box)
32
+ self.main_box.Fit(self.panel)
33
+ self.Fit()
34
+ self.SetMinSize(self.GetSize())
35
+ self.Centre()
36
+
37
+ def add_items(self):
38
+ index=0
39
+ for i in globals.accounts:
40
+ self.list.Insert(i.me.acct,self.list.GetCount())
41
+ if i==globals.currentAccount:
42
+ self.list.SetSelection(index)
43
+ index+=1
44
+
45
+ def on_list_change(self,event):
46
+ pass
47
+
48
+ def New(self, event):
49
+ globals.add_session()
50
+ globals.prefs.accounts+=1
51
+ globals.currentAccount=globals.accounts[len(globals.accounts)-1]
52
+ main.window.refreshTimelines()
53
+ main.window.on_list_change(None)
54
+ main.window.SetLabel(globals.currentAccount.me.acct+" - "+application.name+" "+application.version)
55
+ self.Destroy()
56
+
57
+ def Load(self, event):
58
+ globals.currentAccount=globals.accounts[self.list.GetSelection()]
59
+ main.window.refreshTimelines()
60
+ main.window.list.SetSelection(globals.currentAccount.currentIndex)
61
+ main.window.on_list_change(None)
62
+ main.window.SetLabel(globals.currentAccount.me.acct+" - "+application.name+" "+application.version)
63
+ self.Destroy()
64
+
65
+ def OnClose(self, event):
66
+ self.Destroy()
@@ -0,0 +1,10 @@
1
+ import wx
2
+
3
+ def ask(parent=None, message="", caption="", default_value=""):
4
+ dlg = wx.TextEntryDialog(parent, caption, message, value=default_value)
5
+ dlg.ShowModal()
6
+ result = dlg.GetValue()
7
+ dlg.Destroy()
8
+ return result
9
+
10
+ app = wx.App()
@@ -0,0 +1,93 @@
1
+ from mastodon import MastodonError
2
+ from lights_off import utils
3
+ import wx
4
+ from . import lists, misc, view
5
+
6
+ class ChooseGui(wx.Dialog):
7
+
8
+ #constants for the types we might need to handle
9
+ TYPE_BLOCK="block"
10
+ TYPE_FOLLOW="follow"
11
+ TYPE_LIST = "list"
12
+ TYPE_LIST_R="listr"
13
+ TYPE_MUTE="mute"
14
+ TYPE_PROFILE = "profile"
15
+ TYPE_UNBLOCK="unblock"
16
+ TYPE_UNFOLLOW="unfollow"
17
+ TYPE_UNMUTE="unmute"
18
+ TYPE_URL="url"
19
+ TYPE_USER_TIMELINE="userTimeline"
20
+
21
+ def __init__(self,account,title="Choose",text="Choose a thing",list=[],type=""):
22
+ self.account=account
23
+ self.type=type
24
+ self.returnvalue=""
25
+ wx.Dialog.__init__(self, None, title=title, style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
26
+ self.Bind(wx.EVT_CLOSE, self.OnClose)
27
+ self.panel = wx.Panel(self)
28
+ self.main_box = wx.BoxSizer(wx.VERTICAL)
29
+ self.chooser_label=wx.StaticText(self.panel, -1, title)
30
+ self.main_box.Add(self.chooser_label, 0, wx.LEFT|wx.RIGHT|wx.TOP, 10)
31
+ self.chooser=wx.ComboBox(self.panel,-1,size=(500,-1))
32
+ self.main_box.Add(self.chooser, 0, wx.ALL|wx.EXPAND, 10)
33
+ self.chooser.SetFocus()
34
+ for i in list:
35
+ self.chooser.Insert(i,self.chooser.GetCount())
36
+ self.chooser.SetSelection(0)
37
+ button_row = wx.BoxSizer(wx.HORIZONTAL)
38
+ self.ok = wx.Button(self.panel, wx.ID_DEFAULT, "OK")
39
+ self.ok.SetDefault()
40
+ self.ok.Bind(wx.EVT_BUTTON, self.OK)
41
+ button_row.Add(self.ok, 0, wx.ALL, 5)
42
+ self.close = wx.Button(self.panel, wx.ID_CANCEL, "&Cancel")
43
+ self.close.Bind(wx.EVT_BUTTON, self.OnClose)
44
+ button_row.Add(self.close, 0, wx.ALL, 5)
45
+ self.main_box.Add(button_row, 0, wx.ALL|wx.ALIGN_RIGHT, 5)
46
+ self.panel.SetSizer(self.main_box)
47
+ self.main_box.Fit(self.panel)
48
+ self.Fit()
49
+ self.SetMinSize(self.GetSize())
50
+ self.Centre()
51
+
52
+ def OK(self, event):
53
+ self.returnvalue=self.chooser.GetValue().strip("@")
54
+ self.Destroy()
55
+ if self.type==self.TYPE_PROFILE:
56
+ user=view.UserViewGui(self.account,[utils.lookup_user_name(self.account,self.returnvalue)],self.returnvalue+"'s profile")
57
+ user.Show()
58
+ elif self.type==self.TYPE_URL:
59
+ utils.openURL(self.returnvalue)
60
+ elif self.type==self.TYPE_LIST:
61
+ gui=lists.ListsGui(self.account,utils.lookup_user_name(self.account,self.returnvalue))
62
+ gui.Show()
63
+ elif self.type==self.TYPE_LIST_R:
64
+ gui=lists.ListsGui(self.account,utils.lookup_user_name(self.account,self.returnvalue),False)
65
+ gui.Show()
66
+ elif self.type==self.TYPE_FOLLOW:
67
+ misc.follow_user(self.account,self.returnvalue)
68
+ elif self.type==self.TYPE_UNFOLLOW:
69
+ misc.unfollow_user(self.account,self.returnvalue)
70
+ elif self.type==self.TYPE_BLOCK:
71
+ user=self.account.block(self.returnvalue)
72
+ elif self.type==self.TYPE_UNBLOCK:
73
+ user=self.account.unblock(self.returnvalue)
74
+ elif self.type==self.TYPE_MUTE:
75
+ try:
76
+ self.account.mute(self.returnvalue)
77
+ except MastodonError as e:
78
+ utils.handle_error(e,"Mute")
79
+ elif self.type==self.TYPE_UNMUTE:
80
+ try:
81
+ self.account.unmute(self.returnvalue)
82
+ except MastodonError as e:
83
+ utils.handle_error(e,"Unmute")
84
+ elif self.type==self.TYPE_USER_TIMELINE:
85
+ misc.user_timeline_user(self.account,self.returnvalue)
86
+
87
+ def OnClose(self, event):
88
+ self.Destroy()
89
+
90
+ def chooser(account,title="choose",text="Choose some stuff",list=[],type=""):
91
+ chooser=ChooseGui(account,title,text,list,type)
92
+ chooser.Show()
93
+ return chooser.returnvalue
@@ -0,0 +1,157 @@
1
+ from lights_off import globals
2
+ from . import main
3
+ from lights_off import speak
4
+ from lights_off import utils
5
+ from lights_off import sound
6
+ def register_key(key,name,reg=True):
7
+ if hasattr(main.window,name):
8
+ try:
9
+ if reg:
10
+ main.window.handler.register_key(key,getattr(main.window,name))
11
+ else:
12
+ main.window.handler.unregister_key(key,getattr(main.window,name))
13
+ return True
14
+ except Exception:
15
+ return False
16
+ if hasattr(main.window,"on"+name):
17
+ try:
18
+ if reg:
19
+ main.window.handler.register_key(key,getattr(main.window,"on"+name))
20
+ else:
21
+ main.window.handler.unregister_key(key,getattr(main.window,"on"+name))
22
+ return True
23
+ except Exception:
24
+ return False
25
+ if hasattr(main.window,"On"+name):
26
+ try:
27
+ if reg:
28
+ main.window.handler.register_key(key,getattr(main.window,"On"+name))
29
+ else:
30
+ main.window.handler.unregister_key(key,getattr(main.window,"On"+name))
31
+ return True
32
+ except Exception:
33
+ return False
34
+ if hasattr(inv,name):
35
+ try:
36
+ if reg:
37
+ main.window.handler.register_key(key,getattr(inv,name))
38
+ else:
39
+ main.window.handler.unregister_key(key,getattr(inv,name))
40
+ return True
41
+ except Exception:
42
+ return False
43
+
44
+ class invisible_interface(object):
45
+ def focus_tl(self,sync=False):
46
+ globals.currentAccount.currentTimeline=globals.currentAccount.list_timelines()[globals.currentAccount.currentIndex]
47
+ if not sync and globals.prefs.invisible_sync or sync:
48
+ main.window.list.SetSelection(globals.currentAccount.currentIndex)
49
+ main.window.on_list_change(None)
50
+ extratext=""
51
+ if globals.prefs.position:
52
+ if len(globals.currentAccount.currentTimeline.statuses)==0:
53
+ extratext+="Empty"
54
+ else:
55
+ extratext+=str(globals.currentAccount.currentTimeline.index+1)+" of "+str(len(globals.currentAccount.currentTimeline.statuses))
56
+ if globals.currentAccount.currentTimeline.read:
57
+ extratext+=", Autoread"
58
+ if globals.currentAccount.currentTimeline.mute:
59
+ extratext+=", muted"
60
+ speak.speak(globals.currentAccount.currentTimeline.name+". "+extratext,True)
61
+ if not globals.prefs.invisible_sync and not sync:
62
+ main.window.play_earcon()
63
+
64
+ def focus_tl_item(self):
65
+ if globals.prefs.invisible_sync:
66
+ main.window.list2.SetSelection(globals.currentAccount.currentTimeline.index)
67
+ main.window.on_list2_change(None)
68
+ else:
69
+ if globals.prefs.earcon_audio and len(sound.get_media_urls(utils.find_urls_in_tweet(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]))) > 0:
70
+ sound.play(globals.currentAccount,"media")
71
+ self.speak_item()
72
+
73
+ def speak_item(self):
74
+ if globals.currentAccount.currentTimeline.type!="messages":
75
+ speak.speak(utils.process_tweet(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]),True)
76
+ else:
77
+ speak.speak(utils.process_message(globals.currentAccount.currentTimeline.statuses[globals.currentAccount.currentTimeline.index]),True)
78
+
79
+ def prev_tl(self,sync=False):
80
+ globals.currentAccount.currentIndex-=1
81
+ if globals.currentAccount.currentIndex<0:
82
+ globals.currentAccount.currentIndex=len(globals.currentAccount.list_timelines())-1
83
+ self.focus_tl(sync)
84
+
85
+ def next_tl(self,sync=False):
86
+ globals.currentAccount.currentIndex+=1
87
+ if globals.currentAccount.currentIndex>=len(globals.currentAccount.list_timelines()):
88
+ globals.currentAccount.currentIndex=0
89
+ self.focus_tl(sync)
90
+
91
+ def prev_item(self):
92
+ if globals.currentAccount.currentTimeline.index==0 or len(globals.currentAccount.currentTimeline.statuses)==0:
93
+ sound.play(globals.currentAccount,"boundary")
94
+ if globals.prefs.repeat:
95
+ self.speak_item()
96
+ return
97
+ globals.currentAccount.currentTimeline.index-=1
98
+ self.focus_tl_item()
99
+
100
+ def prev_item_jump(self):
101
+ if globals.currentAccount.currentTimeline.index < 20:
102
+ sound.play(globals.currentAccount,"boundary")
103
+ if globals.prefs.repeat:
104
+ self.speak_item()
105
+ return
106
+ globals.currentAccount.currentTimeline.index -= 20
107
+ self.focus_tl_item()
108
+
109
+ def top_item(self):
110
+ globals.currentAccount.currentTimeline.index=0
111
+ self.focus_tl_item()
112
+
113
+ def next_item(self):
114
+ if globals.currentAccount.currentTimeline.index==len(globals.currentAccount.currentTimeline.statuses)-1 or len(globals.currentAccount.currentTimeline.statuses)==0:
115
+ sound.play(globals.currentAccount,"boundary")
116
+ if globals.prefs.repeat:
117
+ self.speak_item()
118
+ return
119
+ globals.currentAccount.currentTimeline.index+=1
120
+ self.focus_tl_item()
121
+
122
+ def next_item_jump(self):
123
+ if globals.currentAccount.currentTimeline.index >= len(globals.currentAccount.currentTimeline.statuses) - 20:
124
+ sound.play(globals.currentAccount,"boundary")
125
+ if globals.prefs.repeat:
126
+ self.speak_item()
127
+ return
128
+ globals.currentAccount.currentTimeline.index += 20
129
+ self.focus_tl_item()
130
+
131
+ def bottom_item(self):
132
+ globals.currentAccount.currentTimeline.index=len(globals.currentAccount.currentTimeline.statuses)-1
133
+ self.focus_tl_item()
134
+
135
+ def previous_from_user(self):
136
+ main.window.OnPreviousFromUser()
137
+ self.speak_item()
138
+
139
+ def next_from_user(self):
140
+ main.window.OnNextFromUser()
141
+ self.speak_item()
142
+
143
+ def previous_in_thread(self):
144
+ main.window.OnPreviousInThread()
145
+ self.speak_item()
146
+
147
+ def next_in_thread(self):
148
+ main.window.OnNextInThread()
149
+ self.speak_item()
150
+
151
+ def refresh(self,event=None):
152
+ globals.currentAccount.currentTimeline.load(speech=True)
153
+
154
+ def speak_account(self):
155
+ speak.speak(globals.currentAccount.me.acct)
156
+
157
+ inv=invisible_interface()