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.
- lights_off-0.1.0/.gitignore +13 -0
- lights_off-0.1.0/LICENSE +21 -0
- lights_off-0.1.0/PKG-INFO +99 -0
- lights_off-0.1.0/README.md +64 -0
- lights_off-0.1.0/lights_off/GUI/account_options.py +102 -0
- lights_off-0.1.0/lights_off/GUI/accounts.py +66 -0
- lights_off-0.1.0/lights_off/GUI/ask.py +10 -0
- lights_off-0.1.0/lights_off/GUI/chooser.py +93 -0
- lights_off-0.1.0/lights_off/GUI/invisible.py +157 -0
- lights_off-0.1.0/lights_off/GUI/lists.py +173 -0
- lights_off-0.1.0/lights_off/GUI/main.py +682 -0
- lights_off-0.1.0/lights_off/GUI/misc.py +345 -0
- lights_off-0.1.0/lights_off/GUI/options.py +206 -0
- lights_off-0.1.0/lights_off/GUI/poll.py +40 -0
- lights_off-0.1.0/lights_off/GUI/profile.py +57 -0
- lights_off-0.1.0/lights_off/GUI/search.py +41 -0
- lights_off-0.1.0/lights_off/GUI/timelines.py +46 -0
- lights_off-0.1.0/lights_off/GUI/tray.py +42 -0
- lights_off-0.1.0/lights_off/GUI/tweet.py +283 -0
- lights_off-0.1.0/lights_off/GUI/view.py +365 -0
- lights_off-0.1.0/lights_off/SAAPI64.dll +0 -0
- lights_off-0.1.0/lights_off/Tolk.dll +0 -0
- lights_off-0.1.0/lights_off/Tolk.py +64 -0
- lights_off-0.1.0/lights_off/__init__.py +0 -0
- lights_off-0.1.0/lights_off/__main__.py +77 -0
- lights_off-0.1.0/lights_off/api_log.py +54 -0
- lights_off-0.1.0/lights_off/application.py +4 -0
- lights_off-0.1.0/lights_off/globals.py +144 -0
- lights_off-0.1.0/lights_off/keyboard_handler/LICENSE +19 -0
- lights_off-0.1.0/lights_off/keyboard_handler/__init__.py +4 -0
- lights_off-0.1.0/lights_off/keyboard_handler/global_handler.py +9 -0
- lights_off-0.1.0/lights_off/keyboard_handler/key_constants.py +126 -0
- lights_off-0.1.0/lights_off/keyboard_handler/linux.py +72 -0
- lights_off-0.1.0/lights_off/keyboard_handler/main.py +97 -0
- lights_off-0.1.0/lights_off/keyboard_handler/osx.py +59 -0
- lights_off-0.1.0/lights_off/keyboard_handler/windows.py +42 -0
- lights_off-0.1.0/lights_off/keyboard_handler/wx_handler.py +138 -0
- lights_off-0.1.0/lights_off/keymac.keymap +10 -0
- lights_off-0.1.0/lights_off/keymap.keymap +48 -0
- lights_off-0.1.0/lights_off/mastodon_account.py +321 -0
- lights_off-0.1.0/lights_off/nvdaControllerClient64.dll +0 -0
- lights_off-0.1.0/lights_off/sound.py +84 -0
- lights_off-0.1.0/lights_off/sounds/default/boundary.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/close.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/delete.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/error.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/follow.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/home.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/like.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/likes.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/list.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/max_length.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/media.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/mentions.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/messages.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/new.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/notifications.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/open.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/ready.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/search.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_boost.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_message.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_post.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_reply.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_retweet.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/send_tweet.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/unfollow.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/unlike.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/user.ogg +0 -0
- lights_off-0.1.0/lights_off/sounds/default/volume_changed.ogg +0 -0
- lights_off-0.1.0/lights_off/speak.py +25 -0
- lights_off-0.1.0/lights_off/streaming.py +48 -0
- lights_off-0.1.0/lights_off/timeline.py +371 -0
- lights_off-0.1.0/lights_off/utils.py +537 -0
- lights_off-0.1.0/pyproject.toml +85 -0
- lights_off-0.1.0/tests/conftest.py +64 -0
- lights_off-0.1.0/tests/test_mastodon_account.py +467 -0
- lights_off-0.1.0/tests/test_mastodon_live.py +190 -0
- lights_off-0.1.0/tests/test_mastodon_multi_identity.py +74 -0
- lights_off-0.1.0/tests/test_utils.py +943 -0
lights_off-0.1.0/LICENSE
ADDED
|
@@ -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,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()
|