mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-01-15 22:43:19 +01:00
Compare commits
203 Commits
dependabot
...
v2026.01.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9688c20dd9 | |||
| 4d20d7744a | |||
| 29e52288df | |||
| 9ed2f6771e | |||
| 302c22ab9f | |||
| da493a88ea | |||
| 04dca7681b | |||
| 04843616b3 | |||
| 512e2e1684 | |||
| 7a9337c07a | |||
| 15a9df2ca9 | |||
| 4df58f0880 | |||
| 31bab4cf8a | |||
| cb0bb4cf27 | |||
| ccaa0d98ff | |||
| dba26f735c | |||
|
|
d3a12b9016 | ||
|
|
eacce7c197 | ||
|
|
1801d0ae73 | ||
|
|
41e2fc0d10 | ||
|
|
aeac36b65d | ||
|
|
0e4a1cbe65 | ||
|
|
7bf8e1670f | ||
|
|
2efed8d5c6 | ||
| 9b60abb3a0 | |||
|
|
f4c2dd5239 | ||
|
|
067957edbf | ||
|
|
b2b98e38b5 | ||
|
|
9f9ea2fec4 | ||
|
|
eac75d93fd | ||
|
|
39608774e7 | ||
|
|
c182b8517b | ||
|
|
96b5acf4e9 | ||
|
|
01109a6436 | ||
|
|
cb98f74ba8 | ||
|
|
243dfdacb4 | ||
|
|
06732cf9cf | ||
|
|
f39971a8d0 | ||
|
|
5d7994a696 | ||
|
|
5d400f9211 | ||
|
|
2d3590a2a0 | ||
|
|
318d0bcdf9 | ||
|
|
9df29c8b51 | ||
|
|
8abee93749 | ||
|
|
81b1db072c | ||
|
|
d1e20fa776 | ||
|
|
1f679e731d | ||
|
|
7659ee9a9e | ||
|
|
9939713075 | ||
|
|
170d4ebbd5 | ||
|
|
196a71fbd9 | ||
|
|
e48ff3d642 | ||
|
|
39e25532d7 | ||
|
|
25f563464b | ||
|
|
08bb5b41c9 | ||
|
|
486001a46c | ||
|
|
4e255b5f6e | ||
|
|
877040b086 | ||
|
|
c294b967ae | ||
|
|
16cc1fec8b | ||
|
|
fed6f6da60 | ||
|
|
89160497e4 | ||
|
|
beb621de97 | ||
|
|
b46fadd375 | ||
|
|
f0fbfab3b3 | ||
|
|
f83a3e16ce | ||
|
|
b309d23c8b | ||
|
|
d879a9de5c | ||
|
|
b419f8fb23 | ||
|
|
7a4a454376 | ||
|
|
e94ea49142 | ||
|
|
56415c7506 | ||
|
|
c8879e7998 | ||
|
|
8fb30edf31 | ||
|
|
3db6ee3a17 | ||
| e4520a14e8 | |||
| a13e1f1f10 | |||
| 377578dbe2 | |||
| c436fbc944 | |||
| 3af372973d | |||
| de837e15b9 | |||
| 49eaa57027 | |||
|
|
977de1332a | ||
|
|
cbafb7da69 | ||
|
|
966d43ca29 | ||
|
|
193f9bfb66 | ||
|
|
2f77675562 | ||
|
|
b13dd986fc | ||
|
|
4269124758 | ||
|
|
6be0e82610 | ||
|
|
a6e8f2969a | ||
|
|
eb3a038a60 | ||
|
|
bb60eaa2fc | ||
|
|
46654ef054 | ||
|
|
981d98e5e2 | ||
|
|
7eda523520 | ||
|
|
ea54406127 | ||
|
|
1db78da82f | ||
|
|
9c0b7f0c78 | ||
|
|
e62b4d9d77 | ||
|
|
a99f2e9e64 | ||
|
|
0ad32886e1 | ||
|
|
b1bf2ea95f | ||
|
|
b62b83fa00 | ||
|
|
bfd0d938e1 | ||
|
|
4d2d044aa9 | ||
|
|
e8e1c4dedf | ||
|
|
dd23074177 | ||
|
|
127cd5ddf3 | ||
|
|
cdeab8ffc6 | ||
|
|
d2fc571100 | ||
|
|
4a27571e04 | ||
|
|
f3296d646e | ||
|
|
8c0a863770 | ||
|
|
2b8370a377 | ||
|
|
a4d5066156 | ||
| ebabace52d | |||
|
|
ed0cc67ba5 | ||
|
|
dce625547d | ||
|
|
06c370dfd6 | ||
|
|
db83abdbd3 | ||
|
|
11a60c52f3 | ||
|
|
59f08b991e | ||
|
|
3c4e9a8fa7 | ||
|
|
c632743db3 | ||
|
|
6511a3a889 | ||
|
|
66383b8bda | ||
|
|
d24f89947e | ||
|
|
8a2d6631da | ||
|
|
95d0b575c8 | ||
|
|
1e39ce69ef | ||
|
|
905b0fc255 | ||
|
|
3bf39d2349 | ||
|
|
04b43993eb | ||
|
|
321b358a62 | ||
|
|
83d752c24d | ||
|
|
d8c0094003 | ||
|
|
148e831624 | ||
|
|
5112c309ea | ||
|
|
a0642853e8 | ||
|
|
0a4f2e1936 | ||
|
|
3ae3a304a1 | ||
|
|
23e3327029 | ||
|
|
5fd45b5343 | ||
|
|
706c863cd8 | ||
|
|
a7942c4ffe | ||
|
|
7a4d0b9bf1 | ||
|
|
db8983e469 | ||
|
|
1393813c4e | ||
|
|
4ee3363140 | ||
|
|
5dad01130f | ||
|
|
a5ba726230 | ||
|
|
4070c1bb43 | ||
|
|
73d9474679 | ||
|
|
2bf1c68492 | ||
|
|
f60beb3eab | ||
|
|
f00029a154 | ||
|
|
7ed053c929 | ||
|
|
0de60d085a | ||
|
|
e8443cd526 | ||
|
|
12981ee707 | ||
|
|
3d1b9b9c5e | ||
|
|
dd3d1308b7 | ||
|
|
0fdca4d842 | ||
|
|
985e5d42ea | ||
|
|
80f44a99ca | ||
|
|
04692fb708 | ||
|
|
3cb9a8983d | ||
|
|
29dfa709ef | ||
|
|
7dbfeeced9 | ||
|
|
655828946c | ||
|
|
ca91fe1cbe | ||
|
|
c2ce5bde82 | ||
|
|
4bb65fe624 | ||
|
|
28bcb0f52f | ||
|
|
49a8cd4e65 | ||
|
|
e428b41d85 | ||
|
|
a929db05d2 | ||
|
|
a624a97d14 | ||
|
|
60ac77adf5 | ||
|
|
5cd5179c3e | ||
|
|
6c9ef7ea54 | ||
|
|
6872f4fd1c | ||
|
|
a45e7dd7eb | ||
|
|
ea8d8a9296 | ||
|
|
66bc366da3 | ||
|
|
267875c70a | ||
|
|
e68ba5778a | ||
|
|
82bcf1dbbf | ||
|
|
4779c1d004 | ||
|
|
e295e60fc0 | ||
|
|
e383f15ee8 | ||
|
|
aacbdabee3 | ||
|
|
f290bd80f1 | ||
|
|
4a0dbd2140 | ||
|
|
4de969d9cd | ||
|
|
b1ad31b2ee | ||
|
|
92f7bfeebd | ||
|
|
b4288ce51e | ||
|
|
345ec6ffb0 | ||
|
|
436e58dae0 | ||
|
|
48d55de460 | ||
| bbef9d988b |
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: Get python interpreter
|
- name: Get python interpreter
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.13'
|
||||||
|
|
||||||
- name: Install python packages
|
- name: Install python packages
|
||||||
run: python -m pip install -r requirements.txt
|
run: python -m pip install -r requirements.txt
|
||||||
@@ -29,6 +29,12 @@ jobs:
|
|||||||
.\scripts\build.ps1
|
.\scripts\build.ps1
|
||||||
mv src/dist scripts\TWBlue64
|
mv src/dist scripts\TWBlue64
|
||||||
|
|
||||||
|
- name: Install NSIS
|
||||||
|
run: choco install nsis
|
||||||
|
|
||||||
|
- name: Add NSIS to PATH
|
||||||
|
run: echo "C:\Program Files (x86)\NSIS" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||||
|
|
||||||
- name: make installer
|
- name: make installer
|
||||||
run: |
|
run: |
|
||||||
cd scripts
|
cd scripts
|
||||||
|
|||||||
342
CLAUDE.md
Normal file
342
CLAUDE.md
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
TWBlue is an accessible desktop Mastodon client for Windows, built with Python 3.10 and wxPython. It provides two specialized interfaces optimized for screen reader users to interact with Mastodon instances. The application emphasizes accessibility-first design with keyboard navigation, audio feedback, and screen reader integration.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Running from Source
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
For development from source, VLC dependencies are loaded from `../windows-dependencies/{arch}/` where arch is x86 or x64.
|
||||||
|
|
||||||
|
### Installing Dependencies
|
||||||
|
```bash
|
||||||
|
# Install all Python dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Initialize git submodules for Windows dependencies
|
||||||
|
git submodule init
|
||||||
|
git submodule update
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building
|
||||||
|
```bash
|
||||||
|
# Build binary distribution (from src/ directory)
|
||||||
|
python setup.py build
|
||||||
|
|
||||||
|
# Output will be in src/dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run tests using pytest
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Tests are located in src/test/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generating Documentation
|
||||||
|
```bash
|
||||||
|
cd doc
|
||||||
|
python documentation_importer.py
|
||||||
|
python generator.py
|
||||||
|
# Copy generated language folders to src/documentation/
|
||||||
|
# Copy license.txt to src/documentation/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Translation Management
|
||||||
|
```bash
|
||||||
|
# Extract translation strings (from doc/ directory)
|
||||||
|
pybabel extract -o twblue.pot --msgid-bugs-address "manuel@manuelcortez.net" --copyright-holder "MCV software" --input-dirs ../src
|
||||||
|
|
||||||
|
# Note: Translations managed via Weblate at https://weblate.mcvsoftware.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
TWBlue follows an MVC architecture with distinct separation between data access (Sessions), business logic (Controllers), and presentation (wxUI).
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
#### 1. Session Layer (`src/sessions/`)
|
||||||
|
Sessions represent authenticated connections to Mastodon instances. They manage API interactions, OAuth2 authentication, and persistent data storage.
|
||||||
|
|
||||||
|
- **Base Session** (`sessions/base.py`): Abstract base class with configuration management, SQLiteDict persistence, and decorators for login/configuration checks
|
||||||
|
- **Mastodon Session** (`sessions/mastodon/session.py`): Implements Mastodon.py API wrapper, OAuth2 flow, and account credential management
|
||||||
|
- **Streaming** (`sessions/mastodon/streaming.py`): Real-time event listener that publishes to pub/sub system
|
||||||
|
|
||||||
|
Key patterns:
|
||||||
|
- Sessions use `@_require_login` and `@_require_configuration` decorators
|
||||||
|
- Configuration files stored as INI format via configobj in `config/{session_id}/session.conf`
|
||||||
|
- Persistent data (caches, user lists) stored in `config/{session_id}/cache.db` using SQLiteDict
|
||||||
|
- Each session has its own sound system instance
|
||||||
|
|
||||||
|
#### 2. Buffer System (`src/controller/buffers/`)
|
||||||
|
Buffers are the primary data structures for displaying social media content (timelines, mentions, notifications, conversations, etc.).
|
||||||
|
|
||||||
|
**Base Buffer** (`controller/buffers/base/base.py`):
|
||||||
|
- Links buffer UI (wxPanel) with session (API access) and compose functions (data rendering)
|
||||||
|
- Handles keyboard events (F5/F6 for volume, Delete for item removal, Return for URLs)
|
||||||
|
- Manages periodic updates via `start_stream()` function
|
||||||
|
- Each buffer has a `compose_function` that formats API data for display
|
||||||
|
|
||||||
|
**Mastodon Buffers** (`controller/buffers/mastodon/`):
|
||||||
|
- `base.py`: Mastodon-specific base buffer with timeline pagination
|
||||||
|
- `users.py`: Home timeline, mentions buffer
|
||||||
|
- `community.py`: Local/federated timelines
|
||||||
|
- `notifications.py`: System notifications
|
||||||
|
- `conversations.py`: Direct message threads
|
||||||
|
- `search.py`: Search results
|
||||||
|
|
||||||
|
Buffer lifecycle:
|
||||||
|
1. Created by mainController when session initializes
|
||||||
|
2. Added to view (wx.Treebook)
|
||||||
|
3. Periodically updated via `start_stream()` or real-time via pub/sub events
|
||||||
|
4. Destroyed when session ends or buffer removed
|
||||||
|
|
||||||
|
#### 3. Controller Layer (`src/controller/`)
|
||||||
|
Controllers orchestrate application logic and coordinate between sessions, buffers, and UI.
|
||||||
|
|
||||||
|
**Main Controller** (`controller/mainController.py`):
|
||||||
|
- Manages all active buffers and sessions
|
||||||
|
- Binds keyboard shortcuts to actions
|
||||||
|
- Handles pub/sub event subscriptions
|
||||||
|
- Periodically calls `start_stream()` on visible buffers
|
||||||
|
- Provides buffer search methods: `search_buffer()`, `get_current_buffer()`, `get_best_buffer()`
|
||||||
|
|
||||||
|
**Specialized Controllers**:
|
||||||
|
- `settings.py`: Settings dialog management
|
||||||
|
- `userAlias.py` / `userList.py`: User management features
|
||||||
|
- `mastodon/handler.py`: Mastodon-specific operations (filters, etc.)
|
||||||
|
|
||||||
|
#### 4. GUI Layer (`src/wxUI/`)
|
||||||
|
wxPython-based interface with menu-driven navigation and list controls.
|
||||||
|
|
||||||
|
- **Main Frame** (`wxUI/view.py`): Primary window with wx.Treebook for buffers, menu system, system tray integration
|
||||||
|
- **Buffer Panels** (`wxUI/buffers/`): Panel implementations for each buffer type
|
||||||
|
- **Dialogs** (`wxUI/dialogs/`): Post composition, settings, user profiles, filters
|
||||||
|
|
||||||
|
#### 5. Pub/Sub Event System
|
||||||
|
Decoupled communication using PyPubSub 4.0.3.
|
||||||
|
|
||||||
|
Key events:
|
||||||
|
- `mastodon.status_received`: New post received via streaming
|
||||||
|
- `mastodon.status_updated`: Post edited
|
||||||
|
- `mastodon.notification_received`: New notification
|
||||||
|
- `mastodon.conversation_received`: New DM
|
||||||
|
|
||||||
|
Event flow:
|
||||||
|
1. Streaming listener receives API event
|
||||||
|
2. Publishes to topic via `pub.sendMessage()`
|
||||||
|
3. mainController subscribes to topics and routes to appropriate buffer
|
||||||
|
4. Buffer updates its display
|
||||||
|
|
||||||
|
#### 6. Session Manager (`src/sessionmanager/`)
|
||||||
|
Manages session lifecycle (creation, configuration, activation, deletion).
|
||||||
|
|
||||||
|
- `sessionManager.py`: UI for managing multiple accounts
|
||||||
|
- `manager.py`: Persists session list to global config
|
||||||
|
- Handles OAuth2 authorization flow for new accounts
|
||||||
|
- Loads saved sessions on startup
|
||||||
|
|
||||||
|
#### 7. Configuration System (`src/config.py`, `src/config_utils.py`)
|
||||||
|
Hierarchical configuration with defaults and user overrides.
|
||||||
|
|
||||||
|
- Global config: `config/app-configuration.conf` (defaults in `src/app-configuration.defaults`)
|
||||||
|
- Session configs: `config/{session_id}/session.conf` (defaults in `src/mastodon.defaults`)
|
||||||
|
- Keymaps in `src/keymaps/`
|
||||||
|
- Sound packs in `src/sounds/`
|
||||||
|
|
||||||
|
**Path Management** (`src/paths.py`):
|
||||||
|
- Portable mode: Config/logs in application directory
|
||||||
|
- Installed mode: Config/logs in AppData
|
||||||
|
- Detects installation by presence of `Uninstall.exe`
|
||||||
|
|
||||||
|
#### 8. Accessibility Features
|
||||||
|
Built for screen reader users from the ground up.
|
||||||
|
|
||||||
|
- `accessible_output2`: Multi-screen reader support (NVDA, JAWS, SAPI, etc.)
|
||||||
|
- `sound_lib`: Accessible audio playback with spatial audio
|
||||||
|
- `platform_utils`: OS-specific accessibility hooks
|
||||||
|
- `output.py`: Unified interface for speech output
|
||||||
|
- `sound.py`: Sound system with volume control and sound pack management
|
||||||
|
|
||||||
|
#### 9. Keyboard Handling (`src/keyboard_handler/`)
|
||||||
|
Cross-platform keyboard input with global hotkey support.
|
||||||
|
|
||||||
|
- `wx_handler.py`: wxPython integration
|
||||||
|
- `global_handler.py`: System-wide hotkeys
|
||||||
|
- Platform implementations: `windows.py`, `osx.py`, `linux.py`
|
||||||
|
- `keystrokeEditor/`: UI for customizing shortcuts
|
||||||
|
|
||||||
|
### Application Initialization Flow
|
||||||
|
|
||||||
|
From `src/main.py`:
|
||||||
|
1. Setup logging to temp directory, then move to permanent location
|
||||||
|
2. Initialize language handler
|
||||||
|
3. Load global configuration
|
||||||
|
4. Setup sound system
|
||||||
|
5. Setup accessibility output
|
||||||
|
6. Initialize session manager
|
||||||
|
7. Load saved sessions or prompt for account creation
|
||||||
|
8. Create main controller
|
||||||
|
9. Start main event loop
|
||||||
|
|
||||||
|
### Data Flow Patterns
|
||||||
|
|
||||||
|
#### Real-time Update Flow
|
||||||
|
```
|
||||||
|
Mastodon Streaming API
|
||||||
|
→ sessions/mastodon/streaming.py (StreamListener)
|
||||||
|
→ pub.sendMessage("mastodon.status_received", ...)
|
||||||
|
→ controller/mainController.py (subscriber)
|
||||||
|
→ buffer.add_new_item()
|
||||||
|
→ compose_function(item)
|
||||||
|
→ wxUI update
|
||||||
|
```
|
||||||
|
|
||||||
|
#### User Action Flow
|
||||||
|
```
|
||||||
|
Keyboard input
|
||||||
|
→ wx event handler
|
||||||
|
→ buffer.get_event()
|
||||||
|
→ buffer action method (e.g., open_status())
|
||||||
|
→ session.api_call()
|
||||||
|
→ UI update or pub/sub event
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Periodic Update Flow
|
||||||
|
```
|
||||||
|
RepeatingTimer (every N seconds)
|
||||||
|
→ mainController calls buffer.start_stream()
|
||||||
|
→ session.get_timeline_data()
|
||||||
|
→ buffer.put_items_on_list()
|
||||||
|
→ compose_function for each item
|
||||||
|
→ wxUI list control update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Design Patterns and Conventions
|
||||||
|
|
||||||
|
### Compose Functions
|
||||||
|
Buffers use compose functions to render API objects as user-readable strings. Located in `sessions/mastodon/compose.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
compose_function(item, db, relative_times, show_screen_names=False, session=None)
|
||||||
|
# Returns a string representation of the item for display
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Decorators
|
||||||
|
Sessions use decorators to enforce prerequisites:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@baseSession._require_login
|
||||||
|
def post_status(self, text):
|
||||||
|
# Only executes if self.logged == True
|
||||||
|
pass
|
||||||
|
|
||||||
|
@baseSession._require_configuration
|
||||||
|
def get_timeline(self):
|
||||||
|
# Only executes if self.settings != None
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Buffer Naming Convention
|
||||||
|
Buffers have both a `name` (internal identifier) and `account` (associated username):
|
||||||
|
- `name`: e.g., "home_timeline", "mentions", "notifications"
|
||||||
|
- `account`: e.g., "user@mastodon.social"
|
||||||
|
- Buffers are uniquely identified by (name, account) tuple
|
||||||
|
|
||||||
|
### Configuration Hierarchy
|
||||||
|
1. Default values in `src/*.defaults` files
|
||||||
|
2. User overrides in `config/*.conf` files
|
||||||
|
3. Runtime modifications via settings dialogs
|
||||||
|
4. Written back to user config files on change
|
||||||
|
|
||||||
|
## Important Caveats
|
||||||
|
|
||||||
|
### Platform-Specific Code
|
||||||
|
- VLC paths must be set via environment variables when running from source (see `main.py`)
|
||||||
|
- Windows-specific: pywin32, win-inet-pton, winpaths dependencies
|
||||||
|
- Accessibility output works best on Windows with NVDA/JAWS
|
||||||
|
|
||||||
|
### Threading and Event Handling
|
||||||
|
- API calls often wrapped in `call_threaded()` to avoid blocking UI
|
||||||
|
- Streaming runs in background thread and publishes to main thread via pub/sub
|
||||||
|
- wx events must be handled on main thread
|
||||||
|
|
||||||
|
### Session Lifecycle
|
||||||
|
- Sessions must be logged in before buffer creation
|
||||||
|
- Buffers maintain references to sessions via `self.session`
|
||||||
|
- Destroying a session should destroy all associated buffers
|
||||||
|
- Session settings auto-save on write via `settings.write()`
|
||||||
|
|
||||||
|
### Buffer Visibility
|
||||||
|
- Buffers have `invisible` flag for internal/system buffers
|
||||||
|
- Main controller distinguishes between visible buffers (shown in tree) and invisible buffers (used for data access)
|
||||||
|
- Empty buffers serve as account placeholders in tree structure
|
||||||
|
|
||||||
|
### Logging and Debugging
|
||||||
|
- Logs written to temp directory on startup, then moved to permanent location
|
||||||
|
- Binary builds redirect stdout/stderr to `logs/` directory
|
||||||
|
- Source builds use console output
|
||||||
|
- Use `logging.getLogger("module.name")` pattern throughout
|
||||||
|
|
||||||
|
## Build System Details
|
||||||
|
|
||||||
|
### cx_Freeze Configuration (`src/setup.py`)
|
||||||
|
- Target: Win32GUI (suppresses console window)
|
||||||
|
- Includes: keymaps, locales, sounds, documentation, icon, config defaults
|
||||||
|
- Architecture-specific: Loads x86 or x64 dependencies from windows-dependencies submodule
|
||||||
|
- Special handling for enchant dictionaries, VLC plugins, VC++ redistributables
|
||||||
|
|
||||||
|
### NSIS Installer (`scripts/twblue.nsi`)
|
||||||
|
- Expects binary distribution in `scripts/twblue64/`
|
||||||
|
- Creates Start Menu shortcuts, Desktop shortcut (optional)
|
||||||
|
- Registers uninstaller
|
||||||
|
- Checks for running instances before install/uninstall
|
||||||
|
|
||||||
|
### CI/CD (`.github/workflows/release.yml`)
|
||||||
|
- Triggers on version tags (v20*)
|
||||||
|
- Builds on Windows-latest with Python 3.10
|
||||||
|
- Creates both installer (EXE) and portable (ZIP) distributions
|
||||||
|
- Uploads to GitHub releases
|
||||||
|
|
||||||
|
## Mastodon API Integration
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
OAuth2 flow implemented in `sessions/mastodon/session.py`:
|
||||||
|
1. Create application credentials for instance
|
||||||
|
2. Request OAuth authorization URL
|
||||||
|
3. User authorizes in browser
|
||||||
|
4. Exchange code for access token
|
||||||
|
5. Store credentials in session config
|
||||||
|
|
||||||
|
### API Client
|
||||||
|
Uses Mastodon.py 2.1.4 library:
|
||||||
|
- Instance created with base URL and access token
|
||||||
|
- Methods: `status_post()`, `timeline()`, `account()`, etc.
|
||||||
|
- Rate limiting handled by library
|
||||||
|
- Supports multiple instances simultaneously
|
||||||
|
|
||||||
|
### Streaming API
|
||||||
|
Real-time updates via `sessions/mastodon/streaming.py`:
|
||||||
|
- Inherits from `Mastodon.StreamListener`
|
||||||
|
- Connects to user, public, or hashtag streams
|
||||||
|
- Runs in background thread
|
||||||
|
- Events published to main thread via pub/sub
|
||||||
|
|
||||||
|
## Localization
|
||||||
|
|
||||||
|
TWBlue supports 23 languages:
|
||||||
|
- Translation files in `src/locales/{lang}/LC_MESSAGES/twblue.mo`
|
||||||
|
- Uses gettext with `_()` function throughout codebase
|
||||||
|
- Language selection in settings, stored in global config
|
||||||
|
- Babel for extraction and compilation
|
||||||
|
- Weblate for translation management
|
||||||
@@ -2,6 +2,36 @@ TWBlue Changelog
|
|||||||
|
|
||||||
## changes in this version
|
## changes in this version
|
||||||
|
|
||||||
|
In this version, we have focused on expanding content management capabilities within Mastodon. It is now possible to edit sent posts and schedule them for future publication. Additionally, support for reading quoted posts has been implemented, and a new buffer for server announcements is available. On the Core side, visual stability has been prioritized to ensure proper window display, along with an expansion of keyboard shortcuts.
|
||||||
|
|
||||||
|
* Core:
|
||||||
|
* Fixed a critical issue where buffers were not visible on screen in certain configurations. Now the main window maximizes correctly and visual fixes have been applied to ensure content is accessible. ([#886](https://github.com/mcv-software/twblue/issues/886))
|
||||||
|
* Keyboard shortcut improvements: Several shortcuts have been added and fixed to improve efficiency and avoid conflicts:
|
||||||
|
* New shortcuts for the autocomplete users manager and menu bar items. ([#842](https://github.com/mcv-software/twblue/issues/842))
|
||||||
|
* Added a shortcut for the "Restore Template" button in the template editor dialog. ([#841](https://github.com/mcv-software/twblue/issues/841))
|
||||||
|
* New shortcuts for user list and poll dialogs.
|
||||||
|
* Resolved a conflict with the 's' key shortcut used for seeking media.
|
||||||
|
* Updated the shortcut for marking an account as a "Bot" to avoid conflict with the biography field.
|
||||||
|
* Mastodon:
|
||||||
|
* **Post Editing:** It is finally possible to edit Mastodon posts from TWBlue! You can now correct errors in your posts. ([#859](https://github.com/mcv-software/twblue/issues/859))
|
||||||
|
* Safety warning: if you edit a post containing a poll, votes will be reset.
|
||||||
|
* Polls are now correctly displayed as attachments within the edit dialog.
|
||||||
|
* **Scheduled Posts:** It is now possible to schedule your posts to be published at a later time!
|
||||||
|
* Added a "Schedule post" checkbox to the post dialog with date and time pickers.
|
||||||
|
* Implemented validation to ensure posts are scheduled at least 5 minutes in the future, as required by Mastodon.
|
||||||
|
* The default time is automatically set to 6 minutes in the future for convenience.
|
||||||
|
* **Quoted Posts:** Significantly improved the reading and display of quoted posts. TWBlue now structures and reads this content more clearly. ([#860](https://github.com/mcv-software/twblue/issues/860))
|
||||||
|
* **Content Cleaning:** Implemented a more robust HTML filter to remove junk elements or unnecessary CSS classes from post text, offering a cleaner reading experience.
|
||||||
|
* **Mute Conversation:** Enhanced the "Mute Conversation" feature.
|
||||||
|
* Posts from muted conversations will now be visually hidden from the Home timeline immediately upon muting, ensuring a cleaner experience.
|
||||||
|
* Added a new invisible shortcut to toggle mute on the focused conversation: `Alt+Windows+Shift+Delete` (Default) or `Control+Alt+Windows+Backspace` (Windows 10/11).
|
||||||
|
* The action is also available in the context menu of the post.
|
||||||
|
* **Announcements:** Added support for viewing server announcements.
|
||||||
|
* New dedicated buffer for "Announcements" where you can read instance-wide news.
|
||||||
|
* Added ability to dismiss (mark as read) announcements directly from the buffer.
|
||||||
|
|
||||||
|
## Changes in version 2025.3.8
|
||||||
|
|
||||||
In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author.
|
In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author.
|
||||||
|
|
||||||
* Mastodon:
|
* Mastodon:
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
In this version, we have focused on providing initial support for Mastodon filters and pinned posts. From TWBlue, it is now possible to initially use filters for posts in most buffers, as well as manage them (create, edit, and delete filters, in addition to adding keywords). A new variable has also been added for post templates in the invisible interface that allows displaying whether a post has been pinned by its author.
|
In this version, we have focused on expanding content management capabilities within Mastodon. It is now possible to edit sent posts and schedule them for future publication. Additionally, support for reading quoted posts has been implemented, and a new buffer for server announcements is available. On the Core side, visual stability has been prioritized to ensure proper window display, along with an expansion of keyboard shortcuts.
|
||||||
|
|
||||||
|
* Core:
|
||||||
|
* Fixed a critical issue where buffers were not visible on screen in certain configurations. Now the main window maximizes correctly and visual fixes have been applied to ensure content is accessible. ([#886](https://github.com/mcv-software/twblue/issues/886))
|
||||||
|
* Keyboard shortcut improvements: Several shortcuts have been added and fixed to improve efficiency and avoid conflicts:
|
||||||
|
* New shortcuts for the autocomplete users manager and menu bar items. ([#842](https://github.com/mcv-software/twblue/issues/842))
|
||||||
|
* Added a shortcut for the "Restore Template" button in the template editor dialog. ([#841](https://github.com/mcv-software/twblue/issues/841))
|
||||||
|
* New shortcuts for user list and poll dialogs.
|
||||||
|
* Resolved a conflict with the 's' key shortcut used for seeking media.
|
||||||
|
* Updated the shortcut for marking an account as a "Bot" to avoid conflict with the biography field.
|
||||||
* Mastodon:
|
* Mastodon:
|
||||||
* Added filters support to TWBlue. Filters are only implemented in posts for the moment. TWBlue will, depending in the selected settings, hide behind a content warning or completely ignore a post based on filters. Also it is possible to add, delete or edit filters from the buffer menu in the menu bar.
|
* **Post Editing:** It is finally possible to edit Mastodon posts from TWBlue! You can now correct errors in your posts. ([#859](https://github.com/mcv-software/twblue/issues/859))
|
||||||
* A language selector has been added for posting in TWBlue. It is now possible to choose the language in which a post will be made, which will be useful for content filtering and other language-dependent features. The default language can be chosen based on your Mastodon account’s language, the language of the post you’re replying to, or, if no automatic selection is possible, TWBlue’s own language will be used by default.
|
* Safety warning: if you edit a post containing a poll, votes will be reset.
|
||||||
* TWBlue now supports announcing (via a new template variable for posts) pinned posts. You can edit your posts template and use the $pinned variable. When reading the post, TWBlue will indicate if the post is pinned. Also, when loading an user timeline, pinned posts will be loaded at the top or bottom of the buffer according to local settings.
|
* Polls are now correctly displayed as attachments within the edit dialog.
|
||||||
* TWBlue should be able to display all posts in the post displayer dialog.
|
* **Scheduled Posts:** It is now possible to schedule your posts to be published at a later time!
|
||||||
* reading long posts in the graphical user interface should work better.
|
* Added a "Schedule post" checkbox to the post dialog with date and time pickers.
|
||||||
|
* Implemented validation to ensure posts are scheduled at least 5 minutes in the future, as required by Mastodon.
|
||||||
|
* The default time is automatically set to 6 minutes in the future for convenience.
|
||||||
|
* **Quoted Posts:** Significantly improved the reading and display of quoted posts. TWBlue now structures and reads this content more clearly. ([#860](https://github.com/mcv-software/twblue/issues/860))
|
||||||
|
* **Content Cleaning:** Implemented a more robust HTML filter to remove junk elements or unnecessary CSS classes from post text, offering a cleaner reading experience.
|
||||||
|
* **Mute Conversation:** Enhanced the "Mute Conversation" feature.
|
||||||
|
* Posts from muted conversations will now be visually hidden from the Home timeline immediately upon muting, ensuring a cleaner experience.
|
||||||
|
* Added a new invisible shortcut to toggle mute on the focused conversation: `Alt+Windows+Shift+Delete` (Default) or `Control+Alt+Windows+Backspace` (Windows 10/11).
|
||||||
|
* The action is also available in the context menu of the post.
|
||||||
|
* **Announcements:** Added support for viewing server announcements.
|
||||||
|
* New dedicated buffer for "Announcements" where you can read instance-wide news.
|
||||||
|
* Added ability to dismiss (mark as read) announcements directly from the buffer.
|
||||||
@@ -1,58 +1,58 @@
|
|||||||
accessible_output2 @ git+https://github.com/accessibleapps/accessible_output2@57bda997d98e87dd78aa049e7021cf777871619b
|
accessible_output2 @ git+https://github.com/accessibleapps/accessible_output2@57bda997d98e87dd78aa049e7021cf777871619b
|
||||||
arrow==1.3.0
|
arrow==1.4.0
|
||||||
attrs==25.3.0
|
attrs==25.4.0
|
||||||
backports.functools-lru-cache==2.0.0
|
backports.functools-lru-cache==2.0.0
|
||||||
blurhash==1.1.4
|
blurhash==1.1.5
|
||||||
certifi==2025.4.26
|
certifi==2026.1.4
|
||||||
chardet==5.2.0
|
chardet==5.2.0
|
||||||
charset-normalizer==3.4.2
|
charset-normalizer==3.4.4
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
configobj==5.0.9
|
configobj==5.0.9
|
||||||
coverage==7.8.0
|
coverage==7.13.1
|
||||||
cx-Freeze==8.3.0
|
cx-Freeze==8.5.3
|
||||||
cx-Logging==3.2.1
|
cx-Logging==3.2.1
|
||||||
decorator==5.2.1
|
decorator==5.2.1
|
||||||
demoji==1.1.0
|
demoji==1.1.0
|
||||||
deepl==1.22.0
|
deepl==1.27.0
|
||||||
future==1.0.0
|
future==1.0.0
|
||||||
idna==3.10
|
idna==3.11
|
||||||
importlib-metadata==8.7.0
|
importlib-metadata==8.7.1
|
||||||
iniconfig==2.1.0
|
iniconfig==2.3.0
|
||||||
libloader @ git+https://github.com/accessibleapps/libloader@bc94811c095b2e57a036acd88660be9a33260267
|
libloader @ git+https://github.com/accessibleapps/libloader@bc94811c095b2e57a036acd88660be9a33260267
|
||||||
libretranslatepy==2.1.4
|
libretranslatepy==2.1.4
|
||||||
lief==0.15.1
|
lief==0.15.1
|
||||||
Markdown==3.8
|
Markdown==3.10
|
||||||
Mastodon.py==2.0.1
|
Mastodon.py==2.1.4
|
||||||
numpy==2.2.3
|
numpy==2.4.0
|
||||||
oauthlib==3.2.2
|
oauthlib==3.3.1
|
||||||
packaging==25.0
|
packaging==25.0
|
||||||
pillow==11.2.1
|
pillow==12.1.0
|
||||||
platform_utils @ git+https://github.com/accessibleapps/platform_utils@e0d79f7b399c4ea677a633d2dde9202350d62c38
|
platform_utils @ git+https://github.com/accessibleapps/platform_utils@e0d79f7b399c4ea677a633d2dde9202350d62c38
|
||||||
pluggy==1.5.0
|
pluggy==1.6.0
|
||||||
psutil==7.0.0
|
psutil==7.2.1
|
||||||
pyenchant==3.2.2
|
pyenchant==3.3.0
|
||||||
pypiwin32==223
|
pypiwin32==223
|
||||||
Pypubsub==4.0.3
|
Pypubsub==4.0.7
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
pytest==8.3.5
|
pytest==9.0.2
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
python-magic-bin==0.4.14
|
python-magic-bin==0.4.14
|
||||||
python-vlc==3.0.21203
|
python-vlc==3.0.21203
|
||||||
pywin32==310
|
pywin32==311
|
||||||
requests==2.32.3
|
requests==2.32.5
|
||||||
requests-oauthlib==2.0.0
|
requests-oauthlib==2.0.0
|
||||||
requests-toolbelt==1.0.0
|
requests-toolbelt==1.0.0
|
||||||
rfc3986==2.0.0
|
rfc3986==2.0.0
|
||||||
setuptools==78.1.1
|
setuptools==80.9.0
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2
|
sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2
|
||||||
sqlitedict==2.1.0
|
sqlitedict==2.1.0
|
||||||
twitter-text-parser==3.0.0
|
twitter-text-parser==3.0.0
|
||||||
types-python-dateutil==2.9.0.20241206
|
types-python-dateutil==2.9.0.20251115
|
||||||
urllib3==2.4.0
|
urllib3==2.6.3
|
||||||
win-inet-pton==1.1.0
|
win-inet-pton==1.1.0
|
||||||
winpaths==0.2
|
winpaths==0.2
|
||||||
wxPython==4.2.3
|
wxPython==4.2.4
|
||||||
youtube-dl==2021.12.17
|
youtube-dl==2021.12.17
|
||||||
zipp==3.21.0
|
zipp==3.23.0
|
||||||
@@ -6,3 +6,4 @@ from .users import UserBuffer
|
|||||||
from .notifications import NotificationsBuffer
|
from .notifications import NotificationsBuffer
|
||||||
from .search import SearchBuffer
|
from .search import SearchBuffer
|
||||||
from .community import CommunityBuffer
|
from .community import CommunityBuffer
|
||||||
|
from .announcements import AnnouncementsBuffer
|
||||||
165
src/controller/buffers/mastodon/announcements.py
Normal file
165
src/controller/buffers/mastodon/announcements.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import arrow
|
||||||
|
import widgetUtils
|
||||||
|
import wx
|
||||||
|
import output
|
||||||
|
import languageHandler
|
||||||
|
import config
|
||||||
|
from pubsub import pub
|
||||||
|
from controller.buffers.mastodon.base import BaseBuffer
|
||||||
|
from sessions.mastodon import compose, templates
|
||||||
|
from wxUI import buffers
|
||||||
|
from wxUI.dialogs.mastodon import menus
|
||||||
|
from mysc.thread_utils import call_threaded
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.buffers.mastodon.announcements")
|
||||||
|
|
||||||
|
class AnnouncementsBuffer(BaseBuffer):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# We enforce compose_func="compose_announcement"
|
||||||
|
kwargs["compose_func"] = "compose_announcement"
|
||||||
|
super(AnnouncementsBuffer, self).__init__(*args, **kwargs)
|
||||||
|
self.type = "announcementsBuffer"
|
||||||
|
|
||||||
|
def create_buffer(self, parent, name):
|
||||||
|
self.buffer = buffers.mastodon.announcementsPanel(parent, name)
|
||||||
|
|
||||||
|
def get_buffer_name(self):
|
||||||
|
return _("Announcements")
|
||||||
|
|
||||||
|
def bind_events(self):
|
||||||
|
self.buffer.set_focus_function(self.onFocus)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, widgetUtils.KEYPRESS, self.get_event)
|
||||||
|
widgetUtils.connect_event(self.buffer, widgetUtils.BUTTON_PRESSED, self.dismiss_announcement, self.buffer.dismiss)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_ITEM_RIGHT_CLICK, self.show_menu)
|
||||||
|
widgetUtils.connect_event(self.buffer.list.list, wx.EVT_LIST_KEY_DOWN, self.show_menu_by_key)
|
||||||
|
|
||||||
|
def dismiss_announcement(self, event=None, item=None, *args, **kwargs):
|
||||||
|
index = self.buffer.list.get_selected()
|
||||||
|
if index == -1: return
|
||||||
|
item = self.session.db[self.name][index]
|
||||||
|
|
||||||
|
# Optimistic UI update or wait for API? Let's wait for API to be safe, but run threaded.
|
||||||
|
# We need a custom call because 'announcements_dismiss' returns None on success usually.
|
||||||
|
def _do_dismiss():
|
||||||
|
try:
|
||||||
|
self.session.api_call(call_name="announcements_dismiss", id=str(item.id))
|
||||||
|
# If success, update UI in main thread
|
||||||
|
wx.CallAfter(self._on_dismiss_success, index)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error dismissing announcement")
|
||||||
|
self.session.sound.play("error.ogg")
|
||||||
|
|
||||||
|
call_threaded(_do_dismiss)
|
||||||
|
|
||||||
|
def _on_dismiss_success(self, index):
|
||||||
|
if index < len(self.session.db[self.name]):
|
||||||
|
self.session.db[self.name].pop(index)
|
||||||
|
self.buffer.list.remove_item(index)
|
||||||
|
output.speak(_("Announcement dismissed."))
|
||||||
|
|
||||||
|
def show_menu(self, ev, pos=0, *args, **kwargs):
|
||||||
|
if self.buffer.list.get_count() == 0:
|
||||||
|
return
|
||||||
|
# Create a simple menu
|
||||||
|
menu = wx.Menu()
|
||||||
|
dismiss_item = menu.Append(wx.ID_ANY, _("Dismiss"))
|
||||||
|
copy_item = menu.Append(wx.ID_ANY, _("Copy text"))
|
||||||
|
|
||||||
|
self.buffer.Bind(wx.EVT_MENU, self.dismiss_announcement, dismiss_item)
|
||||||
|
self.buffer.Bind(wx.EVT_MENU, self.copy, copy_item)
|
||||||
|
|
||||||
|
if pos != 0:
|
||||||
|
self.buffer.PopupMenu(menu, pos)
|
||||||
|
else:
|
||||||
|
self.buffer.PopupMenu(menu, self.buffer.list.list.GetPosition())
|
||||||
|
|
||||||
|
def url(self, *args, **kwargs):
|
||||||
|
self.dismiss_announcement()
|
||||||
|
|
||||||
|
def get_more_items(self): output.speak(_("This buffer does not support loading more items."), True)
|
||||||
|
|
||||||
|
# Disable social interactions not applicable to announcements
|
||||||
|
def reply(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def share_item(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def toggle_favorite(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_to_favorites(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_from_favorites(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def toggle_bookmark(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mute_conversation(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def vote(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def user_details(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def view_item(self, *args, **kwargs):
|
||||||
|
# We could implement a specific viewer for announcements if needed,
|
||||||
|
# but the default one expects a status object structure.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def copy(self, event=None):
|
||||||
|
item = self.get_item()
|
||||||
|
if item:
|
||||||
|
pub.sendMessage("execute-action", action="copy_to_clipboard")
|
||||||
|
|
||||||
|
def onFocus(self, *args, **kwargs):
|
||||||
|
# Similar logic to BaseBuffer but adapted if needed.
|
||||||
|
# BaseBuffer.onFocus handles reading long posts.
|
||||||
|
if config.app["app-settings"]["read_long_posts_in_gui"] == True and self.buffer.list.list.HasFocus():
|
||||||
|
wx.CallLater(40, output.speak, self.get_message(), interrupt=True)
|
||||||
|
|
||||||
|
def get_message(self):
|
||||||
|
# Override to use announcement template
|
||||||
|
announcement = self.get_item()
|
||||||
|
if announcement == None:
|
||||||
|
return
|
||||||
|
template = self.session.settings.get("templates", {}).get("announcement", templates.announcement_default_template)
|
||||||
|
t = templates.render_announcement(announcement, template, self.session.settings, relative_times=self.session.settings["general"]["relative_times"], offset_hours=self.session.db["utc_offset"])
|
||||||
|
return t
|
||||||
|
|
||||||
|
def start_stream(self, mandatory=False, play_sound=True, avoid_autoreading=False):
|
||||||
|
current_time = time.time()
|
||||||
|
if self.execution_time == 0 or current_time-self.execution_time >= 300 or mandatory==True:
|
||||||
|
self.execution_time = current_time
|
||||||
|
log.debug("Starting stream for announcements buffer")
|
||||||
|
try:
|
||||||
|
# The announcements API does not accept min_id or limit parameters
|
||||||
|
results = self.session.api.announcements()
|
||||||
|
# Reverse the list so order_buffer processes them according to user preference
|
||||||
|
results.reverse()
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Error retrieving announcements: %s" % (str(e)))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# order_buffer handles duplication filtering by ID internally
|
||||||
|
number_of_items = self.session.order_buffer(self.name, results)
|
||||||
|
log.debug("Number of new announcements retrieved: %d" % (number_of_items,))
|
||||||
|
|
||||||
|
self.put_items_on_list(number_of_items)
|
||||||
|
|
||||||
|
if number_of_items > 0 and play_sound == True and self.sound != None and self.session.settings["sound"]["session_mute"] == False:
|
||||||
|
self.session.sound.play(self.sound)
|
||||||
|
|
||||||
|
return number_of_items
|
||||||
|
return 0
|
||||||
@@ -40,9 +40,31 @@ class BaseBuffer(base.Buffer):
|
|||||||
self.buffer.account = account
|
self.buffer.account = account
|
||||||
self.bind_events()
|
self.bind_events()
|
||||||
self.sound = sound
|
self.sound = sound
|
||||||
|
pub.subscribe(self.on_mute_cleanup, "mastodon.mute_cleanup")
|
||||||
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name or "searchterm" in self.name:
|
if "-timeline" in self.name or "-followers" in self.name or "-following" in self.name or "searchterm" in self.name:
|
||||||
self.finished_timeline = False
|
self.finished_timeline = False
|
||||||
|
|
||||||
|
def on_mute_cleanup(self, conversation_id, session_name):
|
||||||
|
if self.name != "home_timeline":
|
||||||
|
return
|
||||||
|
if session_name != self.session.get_name():
|
||||||
|
return
|
||||||
|
items_to_remove = []
|
||||||
|
for index, item in enumerate(self.session.db[self.name]):
|
||||||
|
c_id = None
|
||||||
|
if hasattr(item, "conversation_id"):
|
||||||
|
c_id = item.conversation_id
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
c_id = item.get("conversation_id")
|
||||||
|
|
||||||
|
if c_id == conversation_id:
|
||||||
|
items_to_remove.append(index)
|
||||||
|
|
||||||
|
items_to_remove.sort(reverse=True)
|
||||||
|
for index in items_to_remove:
|
||||||
|
self.session.db[self.name].pop(index)
|
||||||
|
self.buffer.list.remove_item(index)
|
||||||
|
|
||||||
def create_buffer(self, parent, name):
|
def create_buffer(self, parent, name):
|
||||||
self.buffer = buffers.mastodon.basePanel(parent, name)
|
self.buffer = buffers.mastodon.basePanel(parent, name)
|
||||||
|
|
||||||
@@ -280,6 +302,12 @@ class BaseBuffer(base.Buffer):
|
|||||||
return
|
return
|
||||||
menu = menus.base()
|
menu = menus.base()
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
||||||
|
# Enable/disable edit based on whether the post belongs to the user
|
||||||
|
item = self.get_item()
|
||||||
|
if item and item.account.id == self.session.db["user_id"] and item.reblog == None:
|
||||||
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
|
||||||
|
else:
|
||||||
|
menu.edit.Enable(False)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
||||||
if self.can_share() == True:
|
if self.can_share() == True:
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||||
@@ -287,6 +315,7 @@ class BaseBuffer(base.Buffer):
|
|||||||
menu.boost.Enable(False)
|
menu.boost.Enable(False)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.fav, menuitem=menu.fav)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.unfav, menuitem=menu.unfav)
|
||||||
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.mute_conversation, menuitem=menu.mute)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.url_, menuitem=menu.openUrl)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.audio, menuitem=menu.play)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.view, menuitem=menu.view)
|
||||||
@@ -501,6 +530,49 @@ class BaseBuffer(base.Buffer):
|
|||||||
log.exception("")
|
log.exception("")
|
||||||
self.session.db[self.name] = items
|
self.session.db[self.name] = items
|
||||||
|
|
||||||
|
def edit_status(self, event=None, item=None, *args, **kwargs):
|
||||||
|
if item == None:
|
||||||
|
item = self.get_item()
|
||||||
|
# Check if the post belongs to the current user
|
||||||
|
if item.account.id != self.session.db["user_id"] or item.reblog != None:
|
||||||
|
output.speak(_("You can only edit your own posts."))
|
||||||
|
return
|
||||||
|
# Check if post has a poll with votes - warn user before proceeding
|
||||||
|
if hasattr(item, 'poll') and item.poll is not None:
|
||||||
|
votes_count = item.poll.votes_count if hasattr(item.poll, 'votes_count') else 0
|
||||||
|
if votes_count > 0:
|
||||||
|
# Show confirmation dialog
|
||||||
|
warning_title = _("Warning: Poll with votes")
|
||||||
|
warning_message = _("This post contains a poll with {votes} votes.\n\n"
|
||||||
|
"According to Mastodon's API, editing this post will reset ALL votes to zero, "
|
||||||
|
"even if you don't modify the poll itself.\n\n"
|
||||||
|
"Do you want to continue editing?").format(votes=votes_count)
|
||||||
|
dialog = wx.MessageDialog(self.buffer, warning_message, warning_title,
|
||||||
|
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_WARNING)
|
||||||
|
result = dialog.ShowModal()
|
||||||
|
dialog.Destroy()
|
||||||
|
if result != wx.ID_YES:
|
||||||
|
output.speak(_("Edit cancelled"))
|
||||||
|
return
|
||||||
|
# Log item info for debugging
|
||||||
|
log.debug("Editing status: id={}, has_media_attachments={}, media_count={}".format(
|
||||||
|
item.id,
|
||||||
|
hasattr(item, 'media_attachments'),
|
||||||
|
len(item.media_attachments) if hasattr(item, 'media_attachments') else 0
|
||||||
|
))
|
||||||
|
# Create edit dialog with existing post data
|
||||||
|
title = _("Edit post")
|
||||||
|
caption = _("Edit your post here")
|
||||||
|
post = messages.editPost(session=self.session, item=item, title=title, caption=caption)
|
||||||
|
response = post.message.ShowModal()
|
||||||
|
if response == wx.ID_OK:
|
||||||
|
post_data = post.get_data()
|
||||||
|
# Call edit_post method in session
|
||||||
|
# Note: visibility and language cannot be changed when editing per Mastodon API
|
||||||
|
call_threaded(self.session.edit_post, post_id=post.post_id, posts=post_data)
|
||||||
|
if hasattr(post.message, "destroy"):
|
||||||
|
post.message.destroy()
|
||||||
|
|
||||||
def user_details(self):
|
def user_details(self):
|
||||||
item = self.get_item()
|
item = self.get_item()
|
||||||
pass
|
pass
|
||||||
@@ -563,6 +635,22 @@ class BaseBuffer(base.Buffer):
|
|||||||
else:
|
else:
|
||||||
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
|
call_threaded(self.session.api_call, call_name="status_unbookmark", preexec_message=_("Removing from bookmarks..."), _sound="favourite.ogg", id=item.id)
|
||||||
|
|
||||||
|
def mute_conversation(self, event=None, item=None, *args, **kwargs):
|
||||||
|
if item == None:
|
||||||
|
item = self.get_item()
|
||||||
|
if item.reblog != None:
|
||||||
|
item = item.reblog
|
||||||
|
try:
|
||||||
|
item = self.session.api.status(item.id)
|
||||||
|
except MastodonNotFoundError:
|
||||||
|
output.speak(_("No status found with that ID"))
|
||||||
|
return
|
||||||
|
if item.muted == False:
|
||||||
|
call_threaded(self.session.api_call, call_name="status_mute", preexec_message=_("Muting conversation..."), _sound="favourite.ogg", id=item.id)
|
||||||
|
pub.sendMessage("mastodon.mute_cleanup", conversation_id=item.conversation_id, session_name=self.session.get_name())
|
||||||
|
else:
|
||||||
|
call_threaded(self.session.api_call, call_name="status_unmute", preexec_message=_("Unmuting conversation..."), _sound="favourite.ogg", id=item.id)
|
||||||
|
|
||||||
def view_item(self, item=None):
|
def view_item(self, item=None):
|
||||||
if item == None:
|
if item == None:
|
||||||
item = self.get_item()
|
item = self.get_item()
|
||||||
|
|||||||
@@ -161,6 +161,13 @@ class NotificationsBuffer(BaseBuffer):
|
|||||||
menu = menus.notification(notification.type)
|
menu = menus.notification(notification.type)
|
||||||
if self.is_post():
|
if self.is_post():
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.reply, menuitem=menu.reply)
|
||||||
|
# Enable/disable edit based on whether the post belongs to the user
|
||||||
|
if hasattr(menu, 'edit'):
|
||||||
|
status = self.get_post()
|
||||||
|
if status and status.account.id == self.session.db["user_id"] and status.reblog == None:
|
||||||
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.edit_status, menuitem=menu.edit)
|
||||||
|
else:
|
||||||
|
menu.edit.Enable(False)
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.user_actions, menuitem=menu.userActions)
|
||||||
if self.can_share() == True:
|
if self.can_share() == True:
|
||||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||||
|
|||||||
@@ -449,6 +449,15 @@ class Controller(object):
|
|||||||
buffer = self.search_buffer(buffer.name, buffer.account)
|
buffer = self.search_buffer(buffer.name, buffer.account)
|
||||||
buffer.destroy_status()
|
buffer.destroy_status()
|
||||||
|
|
||||||
|
def edit_post(self, *args, **kwargs):
|
||||||
|
""" Edits a post in the current buffer.
|
||||||
|
Users can only edit their own posts."""
|
||||||
|
buffer = self.view.get_current_buffer()
|
||||||
|
if hasattr(buffer, "account"):
|
||||||
|
buffer = self.search_buffer(buffer.name, buffer.account)
|
||||||
|
if hasattr(buffer, "edit_status"):
|
||||||
|
buffer.edit_status()
|
||||||
|
|
||||||
def exit(self, *args, **kwargs):
|
def exit(self, *args, **kwargs):
|
||||||
if config.app["app-settings"]["ask_at_exit"] == True:
|
if config.app["app-settings"]["ask_at_exit"] == True:
|
||||||
answer = commonMessageDialogs.exit_dialog(self.view)
|
answer = commonMessageDialogs.exit_dialog(self.view)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class Handler(object):
|
|||||||
addAlias=_("Add a&lias"),
|
addAlias=_("Add a&lias"),
|
||||||
addToList=None,
|
addToList=None,
|
||||||
removeFromList=None,
|
removeFromList=None,
|
||||||
details=_("Show user profile"),
|
details=_("S&how user profile"),
|
||||||
favs=None,
|
favs=None,
|
||||||
# In buffer Menu.
|
# In buffer Menu.
|
||||||
community_timeline =_("Create c&ommunity timeline"),
|
community_timeline =_("Create c&ommunity timeline"),
|
||||||
@@ -92,6 +92,8 @@ class Handler(object):
|
|||||||
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
|
pub.sendMessage("createBuffer", buffer_type="UserBuffer", session_type=session.type, buffer_title=_("Blocked users"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_user", function="blocks", name="blocked", sessionObject=session, account=name))
|
||||||
elif i == 'notifications':
|
elif i == 'notifications':
|
||||||
pub.sendMessage("createBuffer", buffer_type="NotificationsBuffer", session_type=session.type, buffer_title=_("Notifications"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_notification", function="notifications", name="notifications", sessionObject=session, account=name))
|
pub.sendMessage("createBuffer", buffer_type="NotificationsBuffer", session_type=session.type, buffer_title=_("Notifications"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, compose_func="compose_notification", function="notifications", name="notifications", sessionObject=session, account=name))
|
||||||
|
elif i == 'announcements':
|
||||||
|
pub.sendMessage("createBuffer", buffer_type="AnnouncementsBuffer", session_type=session.type, buffer_title=_("Announcements"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, function="announcements", name="announcements", sessionObject=session, account=name, sound="new_event.ogg"))
|
||||||
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
|
pub.sendMessage("createBuffer", buffer_type="EmptyBuffer", session_type="base", buffer_title=_("Timelines"), parent_tab=root_position, start=False, kwargs=dict(parent=controller.view.nb, name="timelines", account=name))
|
||||||
timelines_position =controller.view.search("timelines", name)
|
timelines_position =controller.view.search("timelines", name)
|
||||||
for i in session.settings["other_buffers"]["timelines"]:
|
for i in session.settings["other_buffers"]["timelines"]:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import wx
|
import wx
|
||||||
|
import logging
|
||||||
import widgetUtils
|
import widgetUtils
|
||||||
import config
|
import config
|
||||||
import output
|
import output
|
||||||
@@ -14,6 +15,8 @@ from wxUI.dialogs.mastodon import postDialogs
|
|||||||
from extra.autocompletionUsers import completion
|
from extra.autocompletionUsers import completion
|
||||||
from . import userList
|
from . import userList
|
||||||
|
|
||||||
|
log = logging.getLogger("controller.mastodon.messages")
|
||||||
|
|
||||||
def character_count(post_text, post_cw, character_limit=500):
|
def character_count(post_text, post_cw, character_limit=500):
|
||||||
# We will use text for counting character limit only.
|
# We will use text for counting character limit only.
|
||||||
full_text = post_text+post_cw
|
full_text = post_text+post_cw
|
||||||
@@ -62,6 +65,13 @@ class post(messages.basicMessage):
|
|||||||
postdata = dict(text=text, attachments=attachments, sensitive=self.message.sensitive.GetValue(), spoiler_text=None)
|
postdata = dict(text=text, attachments=attachments, sensitive=self.message.sensitive.GetValue(), spoiler_text=None)
|
||||||
if postdata.get("sensitive") == True:
|
if postdata.get("sensitive") == True:
|
||||||
postdata.update(spoiler_text=self.message.spoiler.GetValue())
|
postdata.update(spoiler_text=self.message.spoiler.GetValue())
|
||||||
|
|
||||||
|
# Check for scheduled post
|
||||||
|
if hasattr(self.message, 'get_scheduled_at'):
|
||||||
|
scheduled_at = self.message.get_scheduled_at()
|
||||||
|
if scheduled_at:
|
||||||
|
postdata['scheduled_at'] = scheduled_at
|
||||||
|
|
||||||
self.thread.append(postdata)
|
self.thread.append(postdata)
|
||||||
self.attachments = []
|
self.attachments = []
|
||||||
if update_gui:
|
if update_gui:
|
||||||
@@ -262,6 +272,108 @@ class post(messages.basicMessage):
|
|||||||
visibility_setting = visibility_settings.index(setting)
|
visibility_setting = visibility_settings.index(setting)
|
||||||
self.message.visibility.SetSelection(setting)
|
self.message.visibility.SetSelection(setting)
|
||||||
|
|
||||||
|
class editPost(post):
|
||||||
|
def __init__(self, session, item, title, caption, *args, **kwargs):
|
||||||
|
""" Initialize edit dialog with existing post data.
|
||||||
|
|
||||||
|
Note: Per Mastodon API, visibility and language cannot be changed when editing.
|
||||||
|
These fields will be displayed but disabled in the UI.
|
||||||
|
"""
|
||||||
|
# Extract text from post
|
||||||
|
if item.reblog != None:
|
||||||
|
item = item.reblog
|
||||||
|
text = item.content
|
||||||
|
# Remove HTML tags from content
|
||||||
|
import re
|
||||||
|
text = re.sub('<[^<]+?>', '', text)
|
||||||
|
# Initialize parent class
|
||||||
|
super(editPost, self).__init__(session, title, caption, text=text, *args, **kwargs)
|
||||||
|
# Store the post ID for editing
|
||||||
|
self.post_id = item.id
|
||||||
|
# Set visibility (read-only, cannot be changed)
|
||||||
|
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
|
||||||
|
self.message.visibility.SetSelection(visibility_settings.get(item.visibility, 0))
|
||||||
|
self.message.visibility.Enable(False) # Disable as it cannot be edited
|
||||||
|
# Set language (read-only, cannot be changed)
|
||||||
|
if item.language:
|
||||||
|
self.set_language(item.language)
|
||||||
|
self.message.language.Enable(False) # Disable as it cannot be edited
|
||||||
|
# Set sensitive content and spoiler
|
||||||
|
if item.sensitive:
|
||||||
|
self.message.sensitive.SetValue(True)
|
||||||
|
if item.spoiler_text:
|
||||||
|
self.message.spoiler.ChangeValue(item.spoiler_text)
|
||||||
|
self.message.on_sensitivity_changed()
|
||||||
|
# Load existing poll (if any)
|
||||||
|
# Note: You cannot have both media and a poll, so check poll first
|
||||||
|
if hasattr(item, 'poll') and item.poll is not None:
|
||||||
|
log.debug("Loading existing poll for post {}".format(self.post_id))
|
||||||
|
poll = item.poll
|
||||||
|
# Extract poll options (just the text, not the votes)
|
||||||
|
poll_options = [option.title for option in poll.options]
|
||||||
|
# Calculate expires_in based on current time and expires_at
|
||||||
|
# For editing, we need to provide a new expiration time
|
||||||
|
# Since we can't get the original expires_in, use a default or let user configure
|
||||||
|
# For now, use 1 day (86400 seconds) as default
|
||||||
|
expires_in = 86400
|
||||||
|
if hasattr(poll, 'expires_at') and poll.expires_at and not poll.expired:
|
||||||
|
# Calculate remaining time if poll hasn't expired
|
||||||
|
from dateutil import parser as date_parser
|
||||||
|
import datetime
|
||||||
|
try:
|
||||||
|
expires_at = poll.expires_at
|
||||||
|
if isinstance(expires_at, str):
|
||||||
|
expires_at = date_parser.parse(expires_at)
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
remaining = (expires_at - now).total_seconds()
|
||||||
|
if remaining > 0:
|
||||||
|
expires_in = int(remaining)
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Could not calculate poll expiration: {}".format(e))
|
||||||
|
|
||||||
|
poll_info = {
|
||||||
|
"type": "poll",
|
||||||
|
"file": "",
|
||||||
|
"description": _("Poll with {} options").format(len(poll_options)),
|
||||||
|
"options": poll_options,
|
||||||
|
"expires_in": expires_in,
|
||||||
|
"multiple": poll.multiple if hasattr(poll, 'multiple') else False,
|
||||||
|
"hide_totals": poll.voters_count == 0 if hasattr(poll, 'voters_count') else False
|
||||||
|
}
|
||||||
|
self.attachments.append(poll_info)
|
||||||
|
self.message.add_item(item=[poll_info["file"], poll_info["type"], poll_info["description"]])
|
||||||
|
log.debug("Loaded poll with {} options. WARNING: Editing will reset all votes!".format(len(poll_options)))
|
||||||
|
# Load existing media attachments (only if no poll)
|
||||||
|
elif hasattr(item, 'media_attachments'):
|
||||||
|
log.debug("Loading existing media attachments for post {}".format(self.post_id))
|
||||||
|
log.debug("Item has media_attachments attribute, count: {}".format(len(item.media_attachments)))
|
||||||
|
if len(item.media_attachments) > 0:
|
||||||
|
for media in item.media_attachments:
|
||||||
|
log.debug("Processing media: id={}, type={}, url={}".format(media.id, media.type, media.url))
|
||||||
|
media_info = {
|
||||||
|
"id": media.id, # Keep the existing media ID
|
||||||
|
"type": media.type,
|
||||||
|
"file": media.url, # URL of existing media
|
||||||
|
"description": media.description or ""
|
||||||
|
}
|
||||||
|
# Include focus point if available
|
||||||
|
if hasattr(media, 'meta') and media.meta and 'focus' in media.meta:
|
||||||
|
focus = media.meta['focus']
|
||||||
|
media_info["focus"] = (focus.get('x'), focus.get('y'))
|
||||||
|
log.debug("Added focus point: {}".format(media_info["focus"]))
|
||||||
|
self.attachments.append(media_info)
|
||||||
|
# Display in the attachment list
|
||||||
|
display_name = media.url.split('/')[-1]
|
||||||
|
log.debug("Adding item to UI: name={}, type={}, desc={}".format(display_name, media.type, media.description or ""))
|
||||||
|
self.message.add_item(item=[display_name, media.type, media.description or ""])
|
||||||
|
log.debug("Total attachments loaded: {}".format(len(self.attachments)))
|
||||||
|
else:
|
||||||
|
log.debug("media_attachments list is empty")
|
||||||
|
else:
|
||||||
|
log.debug("Item has no poll or media attachments")
|
||||||
|
# Update text processor to reflect the loaded content
|
||||||
|
self.text_processor()
|
||||||
|
|
||||||
class viewPost(post):
|
class viewPost(post):
|
||||||
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
|
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|||||||
@@ -52,10 +52,12 @@ class accountSettingsController(globalSettingsController):
|
|||||||
post_template = self.config["templates"]["post"]
|
post_template = self.config["templates"]["post"]
|
||||||
conversation_template = self.config["templates"]["conversation"]
|
conversation_template = self.config["templates"]["conversation"]
|
||||||
person_template = self.config["templates"]["person"]
|
person_template = self.config["templates"]["person"]
|
||||||
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template)
|
announcement_template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
|
||||||
|
self.dialog.create_templates(post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
|
||||||
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
|
widgetUtils.connect_event(self.dialog.templates.post, widgetUtils.BUTTON_PRESSED, self.edit_post_template)
|
||||||
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
|
widgetUtils.connect_event(self.dialog.templates.conversation, widgetUtils.BUTTON_PRESSED, self.edit_conversation_template)
|
||||||
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
|
widgetUtils.connect_event(self.dialog.templates.person, widgetUtils.BUTTON_PRESSED, self.edit_person_template)
|
||||||
|
widgetUtils.connect_event(self.dialog.templates.announcement, widgetUtils.BUTTON_PRESSED, self.edit_announcement_template)
|
||||||
self.dialog.create_other_buffers()
|
self.dialog.create_other_buffers()
|
||||||
buffer_values = self.get_buffers_list()
|
buffer_values = self.get_buffers_list()
|
||||||
self.dialog.buffers.insert_buffers(buffer_values)
|
self.dialog.buffers.insert_buffers(buffer_values)
|
||||||
@@ -109,6 +111,15 @@ class accountSettingsController(globalSettingsController):
|
|||||||
self.config.write()
|
self.config.write()
|
||||||
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
|
self.dialog.templates.person.SetLabel(_("Edit template for persons. Current template: {}").format(result))
|
||||||
|
|
||||||
|
def edit_announcement_template(self, *args, **kwargs):
|
||||||
|
template = self.config.get("templates", {}).get("announcement", "$text. Published $published_at. $read")
|
||||||
|
control = EditTemplate(template=template, type="announcement")
|
||||||
|
result = control.run_dialog()
|
||||||
|
if result != "": # Template has been saved.
|
||||||
|
self.config["templates"]["announcement"] = result
|
||||||
|
self.config.write()
|
||||||
|
self.dialog.templates.announcement.SetLabel(_("Edit template for announcements. Current template: {}").format(result))
|
||||||
|
|
||||||
def save_configuration(self):
|
def save_configuration(self):
|
||||||
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
|
if self.config["general"]["relative_times"] != self.dialog.get_value("general", "relative_time"):
|
||||||
self.needs_restart = True
|
self.needs_restart = True
|
||||||
@@ -204,6 +215,7 @@ class accountSettingsController(globalSettingsController):
|
|||||||
all_buffers['blocked']=_("Blocked users")
|
all_buffers['blocked']=_("Blocked users")
|
||||||
all_buffers['muted']=_("Muted users")
|
all_buffers['muted']=_("Muted users")
|
||||||
all_buffers['notifications']=_("Notifications")
|
all_buffers['notifications']=_("Notifications")
|
||||||
|
all_buffers['announcements']=_("Announcements")
|
||||||
list_buffers = []
|
list_buffers = []
|
||||||
hidden_buffers=[]
|
hidden_buffers=[]
|
||||||
all_buffers_keys = list(all_buffers.keys())
|
all_buffers_keys = list(all_buffers.keys())
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import re
|
import re
|
||||||
import wx
|
import wx
|
||||||
from typing import List
|
from typing import List
|
||||||
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables
|
from sessions.mastodon.templates import post_variables, conversation_variables, person_variables, announcement_variables
|
||||||
from wxUI.dialogs import templateDialogs
|
from wxUI.dialogs import templateDialogs
|
||||||
|
|
||||||
class EditTemplate(object):
|
class EditTemplate(object):
|
||||||
@@ -13,6 +13,8 @@ class EditTemplate(object):
|
|||||||
self.variables = post_variables
|
self.variables = post_variables
|
||||||
elif type == "conversation":
|
elif type == "conversation":
|
||||||
self.variables = conversation_variables
|
self.variables = conversation_variables
|
||||||
|
elif type == "announcement":
|
||||||
|
self.variables = announcement_variables
|
||||||
else:
|
else:
|
||||||
self.variables = person_variables
|
self.variables = person_variables
|
||||||
self.template: str = template
|
self.template: str = template
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ class autocompletionManageDialog(widgetUtils.BaseDialog):
|
|||||||
self.users = widgets.list(panel, _(u"Username"), _(u"Name"), style=wx.LC_REPORT)
|
self.users = widgets.list(panel, _(u"Username"), _(u"Name"), style=wx.LC_REPORT)
|
||||||
sizer.Add(label, 0, wx.ALL, 5)
|
sizer.Add(label, 0, wx.ALL, 5)
|
||||||
sizer.Add(self.users.list, 0, wx.ALL, 5)
|
sizer.Add(self.users.list, 0, wx.ALL, 5)
|
||||||
self.add = wx.Button(panel, -1, _(u"Add user"))
|
self.add = wx.Button(panel, -1, _(u"&Add user"))
|
||||||
self.remove = wx.Button(panel, -1, _(u"Remove user"))
|
self.remove = wx.Button(panel, -1, _(u"&Remove user"))
|
||||||
optionsBox = wx.BoxSizer(wx.HORIZONTAL)
|
optionsBox = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
optionsBox.Add(self.add, 0, wx.ALL, 5)
|
optionsBox.Add(self.add, 0, wx.ALL, 5)
|
||||||
optionsBox.Add(self.remove, 0, wx.ALL, 5)
|
optionsBox.Add(self.remove, 0, wx.ALL, 5)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ url = string(default="control+win+b")
|
|||||||
go_home = string(default="control+win+home")
|
go_home = string(default="control+win+home")
|
||||||
go_end = string(default="control+win+end")
|
go_end = string(default="control+win+end")
|
||||||
delete = string(default="control+win+delete")
|
delete = string(default="control+win+delete")
|
||||||
|
edit_post = string(default="")
|
||||||
clear_buffer = string(default="control+win+shift+delete")
|
clear_buffer = string(default="control+win+shift+delete")
|
||||||
repeat_item = string(default="control+win+space")
|
repeat_item = string(default="control+win+space")
|
||||||
copy_to_clipboard = string(default="control+win+shift+c")
|
copy_to_clipboard = string(default="control+win+shift+c")
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
|||||||
go_page_down = string(default="control+win+pagedown")
|
go_page_down = string(default="control+win+pagedown")
|
||||||
update_profile = string(default="control+win+shift+p")
|
update_profile = string(default="control+win+shift+p")
|
||||||
delete = string(default="control+win+delete")
|
delete = string(default="control+win+delete")
|
||||||
|
edit_post = string(default="")
|
||||||
clear_buffer = string(default="control+win+shift+delete")
|
clear_buffer = string(default="control+win+shift+delete")
|
||||||
repeat_item = string(default="control+win+space")
|
repeat_item = string(default="control+win+space")
|
||||||
copy_to_clipboard = string(default="control+win+shift+c")
|
copy_to_clipboard = string(default="control+win+shift+c")
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
|||||||
go_page_down = string(default="control+win+pagedown")
|
go_page_down = string(default="control+win+pagedown")
|
||||||
update_profile = string(default="alt+win+p")
|
update_profile = string(default="alt+win+p")
|
||||||
delete = string(default="alt+win+delete")
|
delete = string(default="alt+win+delete")
|
||||||
|
edit_post = string(default="")
|
||||||
clear_buffer = string(default="alt+win+shift+delete")
|
clear_buffer = string(default="alt+win+shift+delete")
|
||||||
repeat_item = string(default="alt+win+space")
|
repeat_item = string(default="alt+win+space")
|
||||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||||
@@ -56,5 +57,6 @@ update_buffer = string(default="control+alt+shift+u")
|
|||||||
ocr_image = string(default="win+alt+o")
|
ocr_image = string(default="win+alt+o")
|
||||||
open_in_browser = string(default="alt+control+win+return")
|
open_in_browser = string(default="alt+control+win+return")
|
||||||
add_alias=string(default="")
|
add_alias=string(default="")
|
||||||
|
mute_conversation=string(default="control+alt+win+back")
|
||||||
find = string(default="control+win+{")
|
find = string(default="control+win+{")
|
||||||
vote=string(default="alt+win+shift+v")
|
vote=string(default="alt+win+shift+v")
|
||||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
|||||||
go_page_down = string(default="control+win+pagedown")
|
go_page_down = string(default="control+win+pagedown")
|
||||||
update_profile = string(default="alt+win+p")
|
update_profile = string(default="alt+win+p")
|
||||||
delete = string(default="alt+win+delete")
|
delete = string(default="alt+win+delete")
|
||||||
|
edit_post = string(default="")
|
||||||
clear_buffer = string(default="alt+win+shift+delete")
|
clear_buffer = string(default="alt+win+shift+delete")
|
||||||
repeat_item = string(default="control+alt+win+space")
|
repeat_item = string(default="control+alt+win+space")
|
||||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||||
@@ -56,5 +57,6 @@ update_buffer = string(default="control+alt+shift+u")
|
|||||||
ocr_image = string(default="win+alt+o")
|
ocr_image = string(default="win+alt+o")
|
||||||
open_in_browser = string(default="alt+control+win+return")
|
open_in_browser = string(default="alt+control+win+return")
|
||||||
add_alias=string(default="")
|
add_alias=string(default="")
|
||||||
|
mute_conversation=string(default="control+alt+win+back")
|
||||||
find = string(default="control+win+{")
|
find = string(default="control+win+{")
|
||||||
vote=string(default="alt+win+shift+v")
|
vote=string(default="alt+win+shift+v")
|
||||||
@@ -56,4 +56,6 @@ configuration = string(default="control+win+o")
|
|||||||
accountConfiguration = string(default="control+win+shift+o")
|
accountConfiguration = string(default="control+win+shift+o")
|
||||||
update_buffer = string(default="control+win+shift+u")
|
update_buffer = string(default="control+win+shift+u")
|
||||||
open_in_browser = string(default="alt+control+win+return")
|
open_in_browser = string(default="alt+control+win+return")
|
||||||
add_alias=string(default="")
|
add_alias=string(default="")
|
||||||
|
mute_conversation=string(default="alt+win+shift+delete")
|
||||||
|
vote=string(default="alt+win+shift+v")
|
||||||
@@ -34,6 +34,7 @@ go_page_up = string(default="control+win+pageup")
|
|||||||
go_page_down = string(default="control+win+pagedown")
|
go_page_down = string(default="control+win+pagedown")
|
||||||
update_profile = string(default="alt+win+p")
|
update_profile = string(default="alt+win+p")
|
||||||
delete = string(default="control+win+delete")
|
delete = string(default="control+win+delete")
|
||||||
|
edit_post = string(default="")
|
||||||
clear_buffer = string(default="control+win+shift+delete")
|
clear_buffer = string(default="control+win+shift+delete")
|
||||||
repeat_item = string(default="control+win+space")
|
repeat_item = string(default="control+win+space")
|
||||||
copy_to_clipboard = string(default="control+win+shift+c")
|
copy_to_clipboard = string(default="control+win+shift+c")
|
||||||
@@ -58,4 +59,5 @@ update_buffer = string(default="control+win+shift+u")
|
|||||||
ocr_image = string(default="win+alt+o")
|
ocr_image = string(default="win+alt+o")
|
||||||
open_in_browser = string(default="alt+control+win+return")
|
open_in_browser = string(default="alt+control+win+return")
|
||||||
add_alias=string(default="")
|
add_alias=string(default="")
|
||||||
|
mute_conversation=string(default="alt+win+shift+delete")
|
||||||
vote=string(default="alt+win+shift+v")
|
vote=string(default="alt+win+shift+v")
|
||||||
@@ -54,4 +54,5 @@ actions = {
|
|||||||
"update_buffer": _(u"Updates the buffer and retrieves possible lost items there."),
|
"update_buffer": _(u"Updates the buffer and retrieves possible lost items there."),
|
||||||
"ocr_image": _(u"Extracts the text from a picture and displays the result in a dialog."),
|
"ocr_image": _(u"Extracts the text from a picture and displays the result in a dialog."),
|
||||||
"add_alias": _("Adds an alias to an user"),
|
"add_alias": _("Adds an alias to an user"),
|
||||||
|
"mute_conversation": _("Mute/Unmute conversation"),
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1,22 +1,23 @@
|
|||||||
# SOME DESCRIPTIVE TITLE.
|
# SOME DESCRIPTIVE TITLE.
|
||||||
# Copyright (C) 2019 ORGANIZATION
|
# Copyright (C) 2019 ORGANIZATION
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2019.
|
||||||
# zvonimir stanecic <zvonimirek222@yandex.com>, 2023.
|
# zvonimir stanecic <zvonimirek222@yandex.com>, 2023, 2025.
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Tw Blue 0.80\n"
|
"Project-Id-Version: Tw Blue 0.80\n"
|
||||||
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
|
"Report-Msgid-Bugs-To: manuel@manuelcortez.net\n"
|
||||||
"POT-Creation-Date: 2025-04-13 01:18+0000\n"
|
"POT-Creation-Date: 2025-04-13 01:18+0000\n"
|
||||||
"PO-Revision-Date: 2023-04-21 07:45+0000\n"
|
"PO-Revision-Date: 2025-08-10 16:08+0000\n"
|
||||||
"Last-Translator: zvonimir stanecic <zvonimirek222@yandex.com>\n"
|
"Last-Translator: zvonimir stanecic <zvonimirek222@yandex.com>\n"
|
||||||
|
"Language-Team: Polish <https://weblate.mcvsoftware.com/projects/twblue/"
|
||||||
|
"twblue/pl/>\n"
|
||||||
"Language: pl\n"
|
"Language: pl\n"
|
||||||
"Language-Team: Polish "
|
|
||||||
"<https://weblate.mcvsoftware.com/projects/twblue/twblue/pl/>\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && "
|
|
||||||
"(n%100<10 || n%100>=20) ? 1 : 2;\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||||
|
"|| n%100>=20) ? 1 : 2;\n"
|
||||||
|
"X-Generator: Weblate 5.10.4\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
#: languageHandler.py:61
|
#: languageHandler.py:61
|
||||||
@@ -101,7 +102,7 @@ msgstr "Domyślne dla użytkownika"
|
|||||||
#: main.py:105
|
#: main.py:105
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "https://twblue.mcvsoftware.com/donate"
|
msgid "https://twblue.mcvsoftware.com/donate"
|
||||||
msgstr "https://twblue.es/donate"
|
msgstr "https://twblue.mcvsoftware.com/donate"
|
||||||
|
|
||||||
#: main.py:118
|
#: main.py:118
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
@@ -246,9 +247,8 @@ msgid "Following for {}"
|
|||||||
msgstr "Śledzący użytkownika {}"
|
msgstr "Śledzący użytkownika {}"
|
||||||
|
|
||||||
#: controller/messages.py:18
|
#: controller/messages.py:18
|
||||||
#, fuzzy
|
|
||||||
msgid "Translated"
|
msgid "Translated"
|
||||||
msgstr "&Przetłumacz"
|
msgstr "Przetłumaczono"
|
||||||
|
|
||||||
#: controller/settings.py:60
|
#: controller/settings.py:60
|
||||||
msgid "System default"
|
msgid "System default"
|
||||||
@@ -540,9 +540,8 @@ msgid "There are no more items in this buffer."
|
|||||||
msgstr "W tym buforze nie ma więcej elementów."
|
msgstr "W tym buforze nie ma więcej elementów."
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:30 wxUI/dialogs/mastodon/updateProfile.py:35
|
#: controller/mastodon/handler.py:30 wxUI/dialogs/mastodon/updateProfile.py:35
|
||||||
#, fuzzy
|
|
||||||
msgid "Update Profile"
|
msgid "Update Profile"
|
||||||
msgstr "&Edytuj profil"
|
msgstr "Zaktualizuj profil"
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:31 wxUI/dialogs/mastodon/search.py:10
|
#: controller/mastodon/handler.py:31 wxUI/dialogs/mastodon/search.py:10
|
||||||
#: wxUI/view.py:19
|
#: wxUI/view.py:19
|
||||||
@@ -615,13 +614,12 @@ msgid "Add a&lias"
|
|||||||
msgstr "Dodaj a&lias"
|
msgstr "Dodaj a&lias"
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:51
|
#: controller/mastodon/handler.py:51
|
||||||
#, fuzzy
|
|
||||||
msgid "Show user profile"
|
msgid "Show user profile"
|
||||||
msgstr "&Pokaż profil użytkownika"
|
msgstr "Pokaż profil użytkownika"
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:54
|
#: controller/mastodon/handler.py:54
|
||||||
msgid "Create c&ommunity timeline"
|
msgid "Create c&ommunity timeline"
|
||||||
msgstr ""
|
msgstr "Stwórz &oś czasu społeczności"
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:55 wxUI/view.py:57
|
#: controller/mastodon/handler.py:55 wxUI/view.py:57
|
||||||
msgid "Create a &filter"
|
msgid "Create a &filter"
|
||||||
@@ -647,10 +645,9 @@ msgstr "Wyszukiwanie {}"
|
|||||||
|
|
||||||
#: controller/mastodon/handler.py:111
|
#: controller/mastodon/handler.py:111
|
||||||
msgid "Communities"
|
msgid "Communities"
|
||||||
msgstr ""
|
msgstr "Społeczności"
|
||||||
|
|
||||||
#: controller/mastodon/handler.py:114
|
#: controller/mastodon/handler.py:114
|
||||||
#, fuzzy
|
|
||||||
msgid "federated"
|
msgid "federated"
|
||||||
msgstr "federowana"
|
msgstr "federowana"
|
||||||
|
|
||||||
@@ -4864,4 +4861,3 @@ msgstr "Dodatki"
|
|||||||
|
|
||||||
#~ msgid "DeepL API Key: "
|
#~ msgid "DeepL API Key: "
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ persist_size = integer(default=0)
|
|||||||
load_cache_in_memory=boolean(default=True)
|
load_cache_in_memory=boolean(default=True)
|
||||||
show_screen_names = boolean(default=False)
|
show_screen_names = boolean(default=False)
|
||||||
hide_emojis = boolean(default=False)
|
hide_emojis = boolean(default=False)
|
||||||
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications'))
|
buffer_order = list(default=list('home', 'local', 'mentions', 'direct_messages', 'sent', 'favorites', 'bookmarks', 'followers', 'following', 'blocked', 'muted', 'notifications', 'announcements'))
|
||||||
boost_mode = string(default="ask")
|
boost_mode = string(default="ask")
|
||||||
disable_streaming = boolean(default=False)
|
disable_streaming = boolean(default=False)
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@ post = string(default="$display_name, $safe_text $image_descriptions $date. $vis
|
|||||||
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
person = string(default="$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||||
conversation = string(default="Conversation with $users. Last message: $last_post")
|
conversation = string(default="Conversation with $users. Last message: $last_post")
|
||||||
notification = string(default="$display_name $text, $date")
|
notification = string(default="$display_name $text, $date")
|
||||||
|
announcement = string(default="$text. Published $published_at. $read")
|
||||||
|
|
||||||
[filters]
|
[filters]
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ def compose_post(post, db, settings, relative_times, show_screen_names, safe=Tru
|
|||||||
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe))
|
text = _("Boosted from @{}: {}").format(post.reblog.account.acct, templates.process_text(post.reblog, safe=safe))
|
||||||
else:
|
else:
|
||||||
text = templates.process_text(post, safe=safe)
|
text = templates.process_text(post, safe=safe)
|
||||||
|
# Handle quoted posts
|
||||||
|
if hasattr(post, 'quote') and post.quote != None and hasattr(post.quote, 'quoted_status') and post.quote.quoted_status != None:
|
||||||
|
quoted_user = post.quote.quoted_status.account.acct
|
||||||
|
quoted_text = templates.process_text(post.quote.quoted_status, safe=safe)
|
||||||
|
text = text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_text)
|
||||||
filtered = utils.evaluate_filters(post=post, current_context="home")
|
filtered = utils.evaluate_filters(post=post, current_context="home")
|
||||||
if filtered != None:
|
if filtered != None:
|
||||||
text = _("hidden by filter {}").format(filtered)
|
text = _("hidden by filter {}").format(filtered)
|
||||||
@@ -79,4 +84,10 @@ def compose_notification(notification, db, settings, relative_times, show_screen
|
|||||||
filtered = utils.evaluate_filters(post=notification, current_context="notifications")
|
filtered = utils.evaluate_filters(post=notification, current_context="notifications")
|
||||||
if filtered != None:
|
if filtered != None:
|
||||||
text = _("hidden by filter {}").format(filtered)
|
text = _("hidden by filter {}").format(filtered)
|
||||||
return [user, text, ts]
|
return [user, text, ts]
|
||||||
|
|
||||||
|
def compose_announcement(announcement, db, settings, relative_times, show_screen_names, safe=False):
|
||||||
|
# Use the default template or a configured one if available
|
||||||
|
template = settings.get("templates", {}).get("announcement", templates.announcement_default_template)
|
||||||
|
text = templates.render_announcement(announcement, template, settings, relative_times, db["utc_offset"])
|
||||||
|
return [text]
|
||||||
@@ -222,9 +222,10 @@ class Session(base.baseSession):
|
|||||||
in_reply_to_id = reply_to
|
in_reply_to_id = reply_to
|
||||||
for obj in posts:
|
for obj in posts:
|
||||||
text = obj.get("text")
|
text = obj.get("text")
|
||||||
|
scheduled_at = obj.get("scheduled_at")
|
||||||
if len(obj["attachments"]) == 0:
|
if len(obj["attachments"]) == 0:
|
||||||
try:
|
try:
|
||||||
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language)
|
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, visibility=visibility, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language, scheduled_at=scheduled_at)
|
||||||
# If it fails, let's basically send an event with all passed info so we will catch it later.
|
# If it fails, let's basically send an event with all passed info so we will catch it later.
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
||||||
@@ -241,13 +242,113 @@ class Session(base.baseSession):
|
|||||||
for i in obj["attachments"]:
|
for i in obj["attachments"]:
|
||||||
media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True)
|
media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True)
|
||||||
media_ids.append(media.id)
|
media_ids.append(media.id)
|
||||||
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language)
|
item = self.api_call(call_name="status_post", status=text, _sound="tweet_send.ogg", in_reply_to_id=in_reply_to_id, media_ids=media_ids, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language, scheduled_at=scheduled_at)
|
||||||
if item != None:
|
if item != None:
|
||||||
in_reply_to_id = item["id"]
|
in_reply_to_id = item["id"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def edit_post(self, post_id, posts=[]):
|
||||||
|
""" Convenience function to edit a post. Only the first item in posts list is used as threads cannot be edited.
|
||||||
|
|
||||||
|
Note: According to Mastodon API, not all fields can be edited. Visibility, language, and reply context cannot be changed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: ID of the status to edit
|
||||||
|
posts: List with post data. Only first item is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Updated status object or None on failure
|
||||||
|
"""
|
||||||
|
if len(posts) == 0:
|
||||||
|
log.warning("edit_post called with empty posts list")
|
||||||
|
return None
|
||||||
|
|
||||||
|
obj = posts[0]
|
||||||
|
text = obj.get("text")
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
log.warning("edit_post called without text content")
|
||||||
|
return None
|
||||||
|
|
||||||
|
media_ids = []
|
||||||
|
media_attributes = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
poll = None
|
||||||
|
# Handle poll attachments
|
||||||
|
if len(obj["attachments"]) == 1 and obj["attachments"][0]["type"] == "poll":
|
||||||
|
poll = self.api.make_poll(
|
||||||
|
options=obj["attachments"][0]["options"],
|
||||||
|
expires_in=obj["attachments"][0]["expires_in"],
|
||||||
|
multiple=obj["attachments"][0]["multiple"],
|
||||||
|
hide_totals=obj["attachments"][0]["hide_totals"]
|
||||||
|
)
|
||||||
|
log.debug("Editing post with poll (this will reset votes)")
|
||||||
|
# Handle media attachments
|
||||||
|
elif len(obj["attachments"]) > 0:
|
||||||
|
for i in obj["attachments"]:
|
||||||
|
# If attachment has an 'id', it's an existing media that we keep
|
||||||
|
if "id" in i:
|
||||||
|
media_ids.append(i["id"])
|
||||||
|
# If existing media has metadata to update, use generate_media_edit_attributes
|
||||||
|
if "description" in i or "focus" in i:
|
||||||
|
media_attr = self.api.generate_media_edit_attributes(
|
||||||
|
id=i["id"],
|
||||||
|
description=i.get("description"),
|
||||||
|
focus=i.get("focus")
|
||||||
|
)
|
||||||
|
media_attributes.append(media_attr)
|
||||||
|
# Otherwise it's a new file to upload
|
||||||
|
elif "file" in i:
|
||||||
|
description = i.get("description", "")
|
||||||
|
focus = i.get("focus", None)
|
||||||
|
media = self.api_call(
|
||||||
|
"media_post",
|
||||||
|
media_file=i["file"],
|
||||||
|
description=description,
|
||||||
|
focus=focus,
|
||||||
|
synchronous=True
|
||||||
|
)
|
||||||
|
media_ids.append(media.id)
|
||||||
|
log.debug("Uploaded new media with id: {}".format(media.id))
|
||||||
|
|
||||||
|
# Prepare parameters for status_update
|
||||||
|
update_params = {
|
||||||
|
"id": post_id,
|
||||||
|
"status": text,
|
||||||
|
"_sound": "tweet_send.ogg",
|
||||||
|
"sensitive": obj.get("sensitive", False),
|
||||||
|
"spoiler_text": obj.get("spoiler_text", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add optional parameters only if provided
|
||||||
|
if media_ids:
|
||||||
|
update_params["media_ids"] = media_ids
|
||||||
|
if media_attributes:
|
||||||
|
update_params["media_attributes"] = media_attributes
|
||||||
|
if poll:
|
||||||
|
update_params["poll"] = poll
|
||||||
|
|
||||||
|
# Call status_update API
|
||||||
|
log.debug("Editing post {} with params: {}".format(post_id, {k: v for k, v in update_params.items() if k not in ["_sound"]}))
|
||||||
|
item = self.api_call(call_name="status_update", **update_params)
|
||||||
|
|
||||||
|
if item:
|
||||||
|
log.info("Successfully edited post {}".format(post_id))
|
||||||
|
return item
|
||||||
|
|
||||||
|
except MastodonAPIError as e:
|
||||||
|
log.exception("Mastodon API error updating post {}: {}".format(post_id, str(e)))
|
||||||
|
output.speak(_("Error editing post: {}").format(str(e)))
|
||||||
|
pub.sendMessage("mastodon.error_edit", name=self.get_name(), post_id=post_id, error=str(e))
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Unexpected error updating post {}: {}".format(post_id, str(e)))
|
||||||
|
output.speak(_("Error editing post: {}").format(str(e)))
|
||||||
|
return None
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
instance = self.settings["mastodon"]["instance"]
|
instance = self.settings["mastodon"]["instance"]
|
||||||
instance = instance.replace("https://", "")
|
instance = instance.replace("https://", "")
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ post_variables = ["date", "display_name", "screen_name", "source", "lang", "safe
|
|||||||
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
|
person_variables = ["display_name", "screen_name", "description", "followers", "following", "favorites", "posts", "created_at"]
|
||||||
conversation_variables = ["users", "last_post"]
|
conversation_variables = ["users", "last_post"]
|
||||||
notification_variables = ["display_name", "screen_name", "text", "date"]
|
notification_variables = ["display_name", "screen_name", "text", "date"]
|
||||||
|
announcement_variables = ["text", "published_at", "updated_at", "starts_at", "ends_at", "read"]
|
||||||
|
|
||||||
# Default, translatable templates.
|
# Default, translatable templates.
|
||||||
post_default_template = _("$display_name, $text $image_descriptions $date. $source")
|
post_default_template = _("$display_name, $text $image_descriptions $date. $source")
|
||||||
dm_sent_default_template = _("Dm to $recipient_display_name, $text $date")
|
dm_sent_default_template = _("Dm to $recipient_display_name, $text $date")
|
||||||
person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
person_default_template = _("$display_name (@$screen_name). $followers followers, $following following, $posts posts. Joined $created_at.")
|
||||||
notification_default_template = _("$display_name $text, $date")
|
notification_default_template = _("$display_name $text, $date")
|
||||||
|
announcement_default_template = _("$text. Published $published_at. $read")
|
||||||
|
|
||||||
def process_date(field, relative_times=True, offset_hours=0):
|
def process_date(field, relative_times=True, offset_hours=0):
|
||||||
original_date = arrow.get(field)
|
original_date = arrow.get(field)
|
||||||
@@ -76,6 +78,13 @@ def render_post(post, template, settings, relative_times=False, offset_hours=0):
|
|||||||
else:
|
else:
|
||||||
text = process_text(post, safe=False)
|
text = process_text(post, safe=False)
|
||||||
safe_text = process_text(post)
|
safe_text = process_text(post)
|
||||||
|
# Handle quoted posts
|
||||||
|
if hasattr(post, 'quote') and post.quote != None and hasattr(post.quote, 'quoted_status') and post.quote.quoted_status != None:
|
||||||
|
quoted_user = post.quote.quoted_status.account.acct
|
||||||
|
quoted_text = process_text(post.quote.quoted_status, safe=False)
|
||||||
|
quoted_safe_text = process_text(post.quote.quoted_status, safe=True)
|
||||||
|
text = text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_text)
|
||||||
|
safe_text = safe_text + " " + _("Quoting @{}: {}").format(quoted_user, quoted_safe_text)
|
||||||
filtered = utils.evaluate_filters(post=post, current_context="home")
|
filtered = utils.evaluate_filters(post=post, current_context="home")
|
||||||
if filtered != None:
|
if filtered != None:
|
||||||
text = _("hidden by filter {}").format(filtered)
|
text = _("hidden by filter {}").format(filtered)
|
||||||
@@ -178,3 +187,23 @@ def render_notification(notification, template, post_template, settings, relativ
|
|||||||
result = Template(_(template)).safe_substitute(**available_data)
|
result = Template(_(template)).safe_substitute(**available_data)
|
||||||
result = result.replace(" . ", "")
|
result = result.replace(" . ", "")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def render_announcement(announcement, template, settings, relative_times=False, offset_hours=0):
|
||||||
|
""" Renders any given announcement according to the passed template. """
|
||||||
|
global announcement_variables
|
||||||
|
available_data = dict()
|
||||||
|
# Process dates
|
||||||
|
for date_field in ["published_at", "updated_at", "starts_at", "ends_at"]:
|
||||||
|
if hasattr(announcement, date_field) and getattr(announcement, date_field) is not None:
|
||||||
|
available_data[date_field] = process_date(getattr(announcement, date_field), relative_times, offset_hours)
|
||||||
|
else:
|
||||||
|
available_data[date_field] = ""
|
||||||
|
|
||||||
|
available_data["text"] = utils.html_filter(announcement.content)
|
||||||
|
if announcement.read:
|
||||||
|
available_data["read"] = _("Read")
|
||||||
|
else:
|
||||||
|
available_data["read"] = _("Unread")
|
||||||
|
|
||||||
|
result = Template(_(template)).safe_substitute(**available_data)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -3,23 +3,47 @@ import demoji
|
|||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
url_re = re.compile('<a\s*href=[\'|"](.*?)[\'"].*?>')
|
url_re = re.compile(r'<a\s*href=[\'|"](.*?)[\'"].*?>')
|
||||||
|
|
||||||
class HTMLFilter(HTMLParser):
|
class HTMLFilter(HTMLParser):
|
||||||
|
# Classes to ignore when parsing HTML
|
||||||
|
IGNORED_CLASSES = ["quote-inline"]
|
||||||
|
|
||||||
text = ""
|
text = ""
|
||||||
first_paragraph = True
|
first_paragraph = True
|
||||||
|
skip_depth = 0 # Track nesting depth of ignored elements
|
||||||
|
|
||||||
def handle_data(self, data):
|
def handle_data(self, data):
|
||||||
self.text += data
|
# Only add data if we're not inside an ignored element
|
||||||
|
if self.skip_depth == 0:
|
||||||
|
self.text += data
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
def handle_starttag(self, tag, attrs):
|
||||||
if tag == "br":
|
# Check if this tag has a class that should be ignored
|
||||||
self.text = self.text+"\n"
|
attrs_dict = dict(attrs)
|
||||||
elif tag == "p":
|
tag_class = attrs_dict.get("class", "")
|
||||||
if self.first_paragraph:
|
|
||||||
self.first_paragraph = False
|
# Check if any ignored class is present in this tag
|
||||||
else:
|
should_skip = any(ignored_class in tag_class for ignored_class in self.IGNORED_CLASSES)
|
||||||
self.text = self.text+"\n\n"
|
|
||||||
|
if should_skip:
|
||||||
|
self.skip_depth += 1
|
||||||
|
elif self.skip_depth == 0: # Only process tags if we're not skipping
|
||||||
|
if tag == "br":
|
||||||
|
self.text = self.text+"\n"
|
||||||
|
elif tag == "p":
|
||||||
|
if self.first_paragraph:
|
||||||
|
self.first_paragraph = False
|
||||||
|
else:
|
||||||
|
self.text = self.text+"\n\n"
|
||||||
|
else:
|
||||||
|
# We're inside a skipped element, increment depth for nested tags
|
||||||
|
self.skip_depth += 1
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
# Decrement skip depth when closing any tag while skipping
|
||||||
|
if self.skip_depth > 0:
|
||||||
|
self.skip_depth -= 1
|
||||||
|
|
||||||
def html_filter(data):
|
def html_filter(data):
|
||||||
f = HTMLFilter()
|
f = HTMLFilter()
|
||||||
@@ -116,6 +140,11 @@ def evaluate_filters(post: dict, current_context: str) -> str | None:
|
|||||||
- None if no applicable filters are found, meaning the post should be shown normally.
|
- None if no applicable filters are found, meaning the post should be shown normally.
|
||||||
"""
|
"""
|
||||||
filters = post.get("filtered", None)
|
filters = post.get("filtered", None)
|
||||||
|
|
||||||
|
# Automatically hide muted conversations from home timeline.
|
||||||
|
if current_context == "home" and post.get("muted") == True:
|
||||||
|
return "hide"
|
||||||
|
|
||||||
if filters == None:
|
if filters == None:
|
||||||
return
|
return
|
||||||
warn_filter_title = None
|
warn_filter_title = None
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
import application
|
import application
|
||||||
import platform
|
import platform
|
||||||
import os
|
import os
|
||||||
from cx_Freeze import setup, Executable, winmsvcr
|
from cx_Freeze import setup, Executable
|
||||||
from requests import certs
|
from requests import certs
|
||||||
|
|
||||||
def get_architecture_files():
|
def get_architecture_files():
|
||||||
@@ -34,7 +34,7 @@ def find_accessible_output2_datafiles():
|
|||||||
|
|
||||||
base = None
|
base = None
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
base = 'Win32GUI'
|
base = 'GUI'
|
||||||
|
|
||||||
build_exe_options = dict(
|
build_exe_options = dict(
|
||||||
build_exe="dist",
|
build_exe="dist",
|
||||||
@@ -51,8 +51,6 @@ executables = [
|
|||||||
Executable('main.py', base=base, target_name="twblue")
|
Executable('main.py', base=base, target_name="twblue")
|
||||||
]
|
]
|
||||||
|
|
||||||
winmsvcr.FILES = ()
|
|
||||||
winmsvcr.FILES_TO_DUPLICATE = ()
|
|
||||||
setup(name=application.name,
|
setup(name=application.name,
|
||||||
version=application.version,
|
version=application.version,
|
||||||
description=application.description,
|
description=application.description,
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
from .base import basePanel
|
from .base import basePanel
|
||||||
from .conversationList import conversationListPanel
|
from .conversationList import conversationListPanel
|
||||||
from .notifications import notificationsPanel
|
from .notifications import notificationsPanel
|
||||||
from .user import userPanel
|
from .user import userPanel
|
||||||
|
from .announcements import announcementsPanel
|
||||||
|
|||||||
36
src/wxUI/buffers/mastodon/announcements.py
Normal file
36
src/wxUI/buffers/mastodon/announcements.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import wx
|
||||||
|
from multiplatform_widgets import widgets
|
||||||
|
|
||||||
|
class announcementsPanel(wx.Panel):
|
||||||
|
|
||||||
|
def set_focus_function(self, f):
|
||||||
|
self.list.list.Bind(wx.EVT_LIST_ITEM_FOCUSED, f)
|
||||||
|
|
||||||
|
def create_list(self):
|
||||||
|
self.list = widgets.list(self, _("Announcement"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
|
self.list.set_windows_size(0, 800)
|
||||||
|
self.list.set_size()
|
||||||
|
|
||||||
|
def __init__(self, parent, name):
|
||||||
|
super(announcementsPanel, self).__init__(parent)
|
||||||
|
self.name = name
|
||||||
|
self.type = "baseBuffer"
|
||||||
|
self.sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
self.create_list()
|
||||||
|
self.dismiss = wx.Button(self, -1, _("Dismiss"))
|
||||||
|
btnSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
||||||
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
|
self.SetSizer(self.sizer)
|
||||||
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
def set_position(self, reversed=False):
|
||||||
|
if reversed == False:
|
||||||
|
self.list.select_item(self.list.get_count()-1)
|
||||||
|
else:
|
||||||
|
self.list.select_item(0)
|
||||||
|
|
||||||
|
def set_focus_in_list(self):
|
||||||
|
self.list.list.SetFocus()
|
||||||
@@ -9,10 +9,10 @@ class basePanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 60)
|
self.list.set_windows_size(0, 200)
|
||||||
self.list.set_windows_size(1, 320)
|
self.list.set_windows_size(1, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(2, 200)
|
||||||
self.list.set_windows_size(3, 84)
|
self.list.set_windows_size(3, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -35,7 +35,7 @@ class basePanel(wx.Panel):
|
|||||||
btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
btnSizer.Add(self.bookmark, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
btnSizer.Add(self.dm, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ class conversationListPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _(u"User"), _(u"Text"), _(u"Date"), _(u"Client"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 60)
|
self.list.set_windows_size(0, 200)
|
||||||
self.list.set_windows_size(1, 320)
|
self.list.set_windows_size(1, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(2, 200)
|
||||||
self.list.set_windows_size(3, 84)
|
self.list.set_windows_size(3, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -27,7 +27,7 @@ class conversationListPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
btnSizer.Add(self.reply, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class notificationsPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _("Text"), _("Date"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 320)
|
self.list.set_windows_size(0, 600)
|
||||||
self.list.set_windows_size(2, 110)
|
self.list.set_windows_size(1, 200)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -25,7 +25,7 @@ class notificationsPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
btnSizer.Add(self.post, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
btnSizer.Add(self.dismiss, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class userPanel(wx.Panel):
|
|||||||
|
|
||||||
def create_list(self):
|
def create_list(self):
|
||||||
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
self.list = widgets.list(self, _("User"), style=wx.LC_REPORT|wx.LC_SINGLE_SEL|wx.LC_VRULES)
|
||||||
self.list.set_windows_size(0, 320)
|
self.list.set_windows_size(0, 600)
|
||||||
self.list.set_size()
|
self.list.set_size()
|
||||||
|
|
||||||
def __init__(self, parent, name):
|
def __init__(self, parent, name):
|
||||||
@@ -23,7 +23,7 @@ class userPanel(wx.Panel):
|
|||||||
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
btnSizer.Add(self.actions, 0, wx.ALL, 5)
|
||||||
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
btnSizer.Add(self.message, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
self.sizer.Add(btnSizer, 0, wx.ALL, 5)
|
||||||
self.sizer.Add(self.list.list, 0, wx.ALL|wx.EXPAND, 5)
|
self.sizer.Add(self.list.list, 1, wx.ALL|wx.EXPAND, 5)
|
||||||
self.SetSizer(self.sizer)
|
self.SetSizer(self.sizer)
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class generalAccount(wx.Panel, baseDialog.BaseWXDialog):
|
|||||||
self.SetSizer(sizer)
|
self.SetSizer(sizer)
|
||||||
|
|
||||||
class templates(wx.Panel, baseDialog.BaseWXDialog):
|
class templates(wx.Panel, baseDialog.BaseWXDialog):
|
||||||
def __init__(self, parent, post_template, conversation_template, person_template):
|
def __init__(self, parent, post_template, conversation_template, person_template, announcement_template):
|
||||||
super(templates, self).__init__(parent)
|
super(templates, self).__init__(parent)
|
||||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
self.post = wx.Button(self, wx.ID_ANY, _("Edit template for &posts. Current template: {}").format(post_template))
|
self.post = wx.Button(self, wx.ID_ANY, _("Edit template for &posts. Current template: {}").format(post_template))
|
||||||
@@ -56,6 +56,8 @@ class templates(wx.Panel, baseDialog.BaseWXDialog):
|
|||||||
sizer.Add(self.conversation, 0, wx.ALL, 5)
|
sizer.Add(self.conversation, 0, wx.ALL, 5)
|
||||||
self.person = wx.Button(self, wx.ID_ANY, _("Edit template for p&ersons. Current template: {}").format(person_template))
|
self.person = wx.Button(self, wx.ID_ANY, _("Edit template for p&ersons. Current template: {}").format(person_template))
|
||||||
sizer.Add(self.person, 0, wx.ALL, 5)
|
sizer.Add(self.person, 0, wx.ALL, 5)
|
||||||
|
self.announcement = wx.Button(self, wx.ID_ANY, _("Edit template for &announcements. Current template: {}").format(announcement_template))
|
||||||
|
sizer.Add(self.announcement, 0, wx.ALL, 5)
|
||||||
self.SetSizer(sizer)
|
self.SetSizer(sizer)
|
||||||
|
|
||||||
class sound(wx.Panel):
|
class sound(wx.Panel):
|
||||||
@@ -152,8 +154,8 @@ class configurationDialog(baseDialog.BaseWXDialog):
|
|||||||
self.buffers = other_buffers(self.notebook)
|
self.buffers = other_buffers(self.notebook)
|
||||||
self.notebook.AddPage(self.buffers, _(u"Buffers"))
|
self.notebook.AddPage(self.buffers, _(u"Buffers"))
|
||||||
|
|
||||||
def create_templates(self, post_template, conversation_template, person_template):
|
def create_templates(self, post_template, conversation_template, person_template, announcement_template):
|
||||||
self.templates = templates(self.notebook, post_template=post_template, conversation_template=conversation_template, person_template=person_template)
|
self.templates = templates(self.notebook, post_template=post_template, conversation_template=conversation_template, person_template=person_template, announcement_template=announcement_template)
|
||||||
self.notebook.AddPage(self.templates, _("Templates"))
|
self.notebook.AddPage(self.templates, _("Templates"))
|
||||||
|
|
||||||
def create_sound(self, output_devices, input_devices, soundpacks):
|
def create_sound(self, output_devices, input_devices, soundpacks):
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class FilterKeywordPanel(wx.Panel):
|
|||||||
button_sizer.Add(self.add_button, 0, wx.RIGHT, 5)
|
button_sizer.Add(self.add_button, 0, wx.RIGHT, 5)
|
||||||
button_sizer.Add(self.remove_button, 0)
|
button_sizer.Add(self.remove_button, 0)
|
||||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
main_sizer.Add(wx.StaticText(self, label=_("Palabras clave a filtrar:")), 0, wx.BOTTOM, 5)
|
main_sizer.Add(wx.StaticText(self, label=_("Keywords to filter:")), 0, wx.BOTTOM, 5)
|
||||||
main_sizer.Add(list_panel, 1, wx.EXPAND | wx.BOTTOM, 5)
|
main_sizer.Add(list_panel, 1, wx.EXPAND | wx.BOTTOM, 5)
|
||||||
main_sizer.Add(input_sizer, 0, wx.EXPAND | wx.BOTTOM, 5)
|
main_sizer.Add(input_sizer, 0, wx.EXPAND | wx.BOTTOM, 5)
|
||||||
main_sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT)
|
main_sizer.Add(button_sizer, 0, wx.ALIGN_RIGHT)
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ class base(wx.Menu):
|
|||||||
self.Append(self.boost)
|
self.Append(self.boost)
|
||||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||||
self.Append(self.reply)
|
self.Append(self.reply)
|
||||||
|
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))
|
||||||
|
self.Append(self.edit)
|
||||||
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
||||||
self.Append(self.fav)
|
self.Append(self.fav)
|
||||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||||
self.Append(self.unfav)
|
self.Append(self.unfav)
|
||||||
|
self.mute = wx.MenuItem(self, wx.ID_ANY, _(u"Mute/Unmute conversation"))
|
||||||
|
self.Append(self.mute)
|
||||||
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
|
self.openUrl = wx.MenuItem(self, wx.ID_ANY, _("&Open URL"))
|
||||||
self.Append(self.openUrl)
|
self.Append(self.openUrl)
|
||||||
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance"))
|
self.openInBrowser = wx.MenuItem(self, wx.ID_ANY, _(u"&Open in instance"))
|
||||||
@@ -36,6 +40,8 @@ class notification(wx.Menu):
|
|||||||
self.Append(self.boost)
|
self.Append(self.boost)
|
||||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||||
self.Append(self.reply)
|
self.Append(self.reply)
|
||||||
|
self.edit = wx.MenuItem(self, wx.ID_ANY, _(u"&Edit"))
|
||||||
|
self.Append(self.edit)
|
||||||
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
self.fav = wx.MenuItem(self, wx.ID_ANY, _(u"&Add to favorites"))
|
||||||
self.Append(self.fav)
|
self.Append(self.fav)
|
||||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import wx
|
import wx
|
||||||
|
import wx.adv
|
||||||
|
import datetime
|
||||||
|
|
||||||
class Post(wx.Dialog):
|
class Post(wx.Dialog):
|
||||||
def __init__(self, caption=_("Post"), text="", languages=[], *args, **kwds):
|
def __init__(self, caption=_("Post"), text="", languages=[], *args, **kwds):
|
||||||
@@ -51,7 +53,7 @@ class Post(wx.Dialog):
|
|||||||
visibility_sizer.Add(self.visibility, 0, 0, 0)
|
visibility_sizer.Add(self.visibility, 0, 0, 0)
|
||||||
language_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
language_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
post_actions_sizer.Add(language_sizer, 0, wx.RIGHT, 20)
|
post_actions_sizer.Add(language_sizer, 0, wx.RIGHT, 20)
|
||||||
lang_label = wx.StaticText(self, wx.ID_ANY, _("Language"))
|
lang_label = wx.StaticText(self, wx.ID_ANY, _("&Language"))
|
||||||
language_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
language_sizer.Add(lang_label, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
|
||||||
self.language = wx.ComboBox(self, wx.ID_ANY, choices=languages, style=wx.CB_DROPDOWN | wx.CB_READONLY)
|
self.language = wx.ComboBox(self, wx.ID_ANY, choices=languages, style=wx.CB_DROPDOWN | wx.CB_READONLY)
|
||||||
language_sizer.Add(self.language, 0, wx.ALIGN_CENTER_VERTICAL, 0)
|
language_sizer.Add(self.language, 0, wx.ALIGN_CENTER_VERTICAL, 0)
|
||||||
@@ -60,6 +62,28 @@ class Post(wx.Dialog):
|
|||||||
self.sensitive.SetValue(False)
|
self.sensitive.SetValue(False)
|
||||||
self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed)
|
self.sensitive.Bind(wx.EVT_CHECKBOX, self.on_sensitivity_changed)
|
||||||
main_sizer.Add(self.sensitive, 0, wx.ALL, 5)
|
main_sizer.Add(self.sensitive, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
# Scheduled post section
|
||||||
|
scheduled_box = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
self.scheduled = wx.CheckBox(self, wx.ID_ANY, _("Schedule &post"))
|
||||||
|
self.scheduled.SetValue(False)
|
||||||
|
self.scheduled.Bind(wx.EVT_CHECKBOX, self.on_schedule_changed)
|
||||||
|
scheduled_box.Add(self.scheduled, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
# Default to now + 6 minutes to be safe for the 5 minute minimum
|
||||||
|
future_dt = wx.DateTime.Now()
|
||||||
|
future_dt.Add(wx.TimeSpan(0, 6, 0, 0))
|
||||||
|
|
||||||
|
self.date_picker = wx.adv.DatePickerCtrl(self, wx.ID_ANY, dt=future_dt, style=wx.adv.DP_DROPDOWN | wx.adv.DP_SHOWCENTURY)
|
||||||
|
self.date_picker.Enable(False)
|
||||||
|
scheduled_box.Add(self.date_picker, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
self.time_picker = wx.adv.TimePickerCtrl(self, wx.ID_ANY, dt=future_dt)
|
||||||
|
self.time_picker.Enable(False)
|
||||||
|
scheduled_box.Add(self.time_picker, 0, wx.ALL, 5)
|
||||||
|
|
||||||
|
main_sizer.Add(scheduled_box, 0, wx.ALL, 5)
|
||||||
|
|
||||||
spoiler_box = wx.BoxSizer(wx.HORIZONTAL)
|
spoiler_box = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
spoiler_label = wx.StaticText(self, wx.ID_ANY, _("Content warning"))
|
spoiler_label = wx.StaticText(self, wx.ID_ANY, _("Content warning"))
|
||||||
self.spoiler = wx.TextCtrl(self, wx.ID_ANY)
|
self.spoiler = wx.TextCtrl(self, wx.ID_ANY)
|
||||||
@@ -80,8 +104,9 @@ class Post(wx.Dialog):
|
|||||||
text_actions_sizer.Add(self.translate, 0, 0, 0)
|
text_actions_sizer.Add(self.translate, 0, 0, 0)
|
||||||
btn_sizer = wx.StdDialogButtonSizer()
|
btn_sizer = wx.StdDialogButtonSizer()
|
||||||
main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
|
main_sizer.Add(btn_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 4)
|
||||||
self.send = wx.Button(self, wx.ID_OK, "")
|
self.send = wx.Button(self, wx.ID_ANY, _("&Send"))
|
||||||
self.send.SetDefault()
|
self.send.SetDefault()
|
||||||
|
self.send.Bind(wx.EVT_BUTTON, self.validate_and_send)
|
||||||
btn_sizer.AddButton(self.send)
|
btn_sizer.AddButton(self.send)
|
||||||
self.close = wx.Button(self, wx.ID_CLOSE, "")
|
self.close = wx.Button(self, wx.ID_CLOSE, "")
|
||||||
btn_sizer.AddButton(self.close)
|
btn_sizer.AddButton(self.close)
|
||||||
@@ -95,13 +120,50 @@ class Post(wx.Dialog):
|
|||||||
""" Allows to react to certain keyboard events from the text control. """
|
""" Allows to react to certain keyboard events from the text control. """
|
||||||
shift=event.ShiftDown()
|
shift=event.ShiftDown()
|
||||||
if event.GetKeyCode() == wx.WXK_RETURN and shift==False and hasattr(self,'send'):
|
if event.GetKeyCode() == wx.WXK_RETURN and shift==False and hasattr(self,'send'):
|
||||||
self.EndModal(wx.ID_OK)
|
self.validate_and_send()
|
||||||
else:
|
else:
|
||||||
event.Skip()
|
event.Skip()
|
||||||
|
|
||||||
|
def validate_and_send(self, event=None):
|
||||||
|
scheduled_at = self.get_scheduled_at()
|
||||||
|
if scheduled_at:
|
||||||
|
min_time = datetime.datetime.now() + datetime.timedelta(minutes=5)
|
||||||
|
if scheduled_at < min_time:
|
||||||
|
wx.MessageDialog(self,
|
||||||
|
_("Scheduled posts must be set at least 5 minutes in the future. Please adjust the time."),
|
||||||
|
_("Invalid scheduled time"),
|
||||||
|
wx.ICON_ERROR | wx.OK).ShowModal()
|
||||||
|
return
|
||||||
|
self.EndModal(wx.ID_OK)
|
||||||
|
|
||||||
def on_sensitivity_changed(self, *args, **kwargs):
|
def on_sensitivity_changed(self, *args, **kwargs):
|
||||||
self.spoiler.Enable(self.sensitive.GetValue())
|
self.spoiler.Enable(self.sensitive.GetValue())
|
||||||
|
|
||||||
|
def on_schedule_changed(self, *args, **kwargs):
|
||||||
|
enabled = self.scheduled.GetValue()
|
||||||
|
self.date_picker.Enable(enabled)
|
||||||
|
self.time_picker.Enable(enabled)
|
||||||
|
|
||||||
|
def get_scheduled_at(self):
|
||||||
|
if not self.scheduled.GetValue():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get date from date picker
|
||||||
|
wx_date = self.date_picker.GetValue()
|
||||||
|
# Get time from time picker
|
||||||
|
wx_time = self.time_picker.GetValue()
|
||||||
|
|
||||||
|
# Combine into a python datetime object
|
||||||
|
dt = datetime.datetime(
|
||||||
|
wx_date.GetYear(),
|
||||||
|
wx_date.GetMonth() + 1, # wx.DateTime months are 0-11
|
||||||
|
wx_date.GetDay(),
|
||||||
|
wx_time.GetHour(),
|
||||||
|
wx_time.GetMinute(),
|
||||||
|
wx_time.GetSecond()
|
||||||
|
)
|
||||||
|
return dt
|
||||||
|
|
||||||
def set_title(self, chars):
|
def set_title(self, chars):
|
||||||
self.SetTitle(_("Post - {} characters").format(chars))
|
self.SetTitle(_("Post - {} characters").format(chars))
|
||||||
|
|
||||||
@@ -234,9 +296,9 @@ class viewPost(wx.Dialog):
|
|||||||
|
|
||||||
def create_buttons_section(self, panel):
|
def create_buttons_section(self, panel):
|
||||||
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
self.mute = wx.Button(panel, wx.ID_ANY, _("Mute conversation"))
|
self.mute = wx.Button(panel, wx.ID_ANY, _("&Mute conversation"))
|
||||||
self.mute.Enable(False)
|
self.mute.Enable(False)
|
||||||
self.share = wx.Button(panel, wx.ID_ANY, _("Copy link to clipboard"))
|
self.share = wx.Button(panel, wx.ID_ANY, _("&Copy link to clipboard"))
|
||||||
self.share.Enable(False)
|
self.share.Enable(False)
|
||||||
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
||||||
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
||||||
@@ -295,7 +357,7 @@ class poll(wx.Dialog):
|
|||||||
sizer_1 = wx.BoxSizer(wx.VERTICAL)
|
sizer_1 = wx.BoxSizer(wx.VERTICAL)
|
||||||
period_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
period_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
sizer_1.Add(period_sizer, 1, wx.EXPAND, 0)
|
sizer_1.Add(period_sizer, 1, wx.EXPAND, 0)
|
||||||
label_period = wx.StaticText(self, wx.ID_ANY, _("Participation time"))
|
label_period = wx.StaticText(self, wx.ID_ANY, _("&Participation time"))
|
||||||
period_sizer.Add(label_period, 0, 0, 0)
|
period_sizer.Add(label_period, 0, 0, 0)
|
||||||
self.period = wx.ComboBox(self, wx.ID_ANY, choices=[_("5 minutes"), _("30 minutes"), _("1 hour"), _("6 hours"), _("1 day"), _("2 days"), _("3 days"), _("4 days"), _("5 days"), _("6 days"), _("7 days")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
|
self.period = wx.ComboBox(self, wx.ID_ANY, choices=[_("5 minutes"), _("30 minutes"), _("1 hour"), _("6 hours"), _("1 day"), _("2 days"), _("3 days"), _("4 days"), _("5 days"), _("6 days"), _("7 days")], style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.CB_SIMPLE)
|
||||||
self.period.SetFocus()
|
self.period.SetFocus()
|
||||||
@@ -305,36 +367,36 @@ class poll(wx.Dialog):
|
|||||||
sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
|
sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
|
||||||
option1_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
option1_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
sizer_2.Add(option1_sizer, 1, wx.EXPAND, 0)
|
sizer_2.Add(option1_sizer, 1, wx.EXPAND, 0)
|
||||||
label_2 = wx.StaticText(self, wx.ID_ANY, _("Option 1"))
|
label_2 = wx.StaticText(self, wx.ID_ANY, _("Option &1"))
|
||||||
option1_sizer.Add(label_2, 0, 0, 0)
|
option1_sizer.Add(label_2, 0, 0, 0)
|
||||||
self.option1 = wx.TextCtrl(self, wx.ID_ANY, "")
|
self.option1 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||||
self.option1.SetMaxLength(25)
|
self.option1.SetMaxLength(25)
|
||||||
option1_sizer.Add(self.option1, 0, 0, 0)
|
option1_sizer.Add(self.option1, 0, 0, 0)
|
||||||
option2_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
option2_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
sizer_2.Add(option2_sizer, 1, wx.EXPAND, 0)
|
sizer_2.Add(option2_sizer, 1, wx.EXPAND, 0)
|
||||||
label_3 = wx.StaticText(self, wx.ID_ANY, _("Option 2"))
|
label_3 = wx.StaticText(self, wx.ID_ANY, _("Option &2"))
|
||||||
option2_sizer.Add(label_3, 0, 0, 0)
|
option2_sizer.Add(label_3, 0, 0, 0)
|
||||||
self.option2 = wx.TextCtrl(self, wx.ID_ANY, "")
|
self.option2 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||||
self.option2.SetMaxLength(25)
|
self.option2.SetMaxLength(25)
|
||||||
option2_sizer.Add(self.option2, 0, 0, 0)
|
option2_sizer.Add(self.option2, 0, 0, 0)
|
||||||
option3_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
option3_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
sizer_2.Add(option3_sizer, 1, wx.EXPAND, 0)
|
sizer_2.Add(option3_sizer, 1, wx.EXPAND, 0)
|
||||||
label_4 = wx.StaticText(self, wx.ID_ANY, _("Option 3"))
|
label_4 = wx.StaticText(self, wx.ID_ANY, _("Option &3"))
|
||||||
option3_sizer.Add(label_4, 0, 0, 0)
|
option3_sizer.Add(label_4, 0, 0, 0)
|
||||||
self.option3 = wx.TextCtrl(self, wx.ID_ANY, "")
|
self.option3 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||||
self.option3.SetMaxLength(25)
|
self.option3.SetMaxLength(25)
|
||||||
option3_sizer.Add(self.option3, 0, 0, 0)
|
option3_sizer.Add(self.option3, 0, 0, 0)
|
||||||
option4_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
option4_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
sizer_2.Add(option4_sizer, 1, wx.EXPAND, 0)
|
sizer_2.Add(option4_sizer, 1, wx.EXPAND, 0)
|
||||||
label_5 = wx.StaticText(self, wx.ID_ANY, _("Option 4"))
|
label_5 = wx.StaticText(self, wx.ID_ANY, _("Option &4"))
|
||||||
option4_sizer.Add(label_5, 0, 0, 0)
|
option4_sizer.Add(label_5, 0, 0, 0)
|
||||||
self.option4 = wx.TextCtrl(self, wx.ID_ANY, "")
|
self.option4 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||||
self.option4.SetMaxLength(25)
|
self.option4.SetMaxLength(25)
|
||||||
option4_sizer.Add(self.option4, 0, 0, 0)
|
option4_sizer.Add(self.option4, 0, 0, 0)
|
||||||
self.multiple = wx.CheckBox(self, wx.ID_ANY, _("Allow multiple choices per user"))
|
self.multiple = wx.CheckBox(self, wx.ID_ANY, _("&Allow multiple choices per user"))
|
||||||
self.multiple.SetValue(False)
|
self.multiple.SetValue(False)
|
||||||
sizer_1.Add(self.multiple, 0, wx.ALL, 5)
|
sizer_1.Add(self.multiple, 0, wx.ALL, 5)
|
||||||
self.hide_votes = wx.CheckBox(self, wx.ID_ANY, _("Hide votes count until the poll expires"))
|
self.hide_votes = wx.CheckBox(self, wx.ID_ANY, _("&Hide votes count until the poll expires"))
|
||||||
self.hide_votes.SetValue(False)
|
self.hide_votes.SetValue(False)
|
||||||
sizer_1.Add(self.hide_votes, 0, wx.ALL, 5)
|
sizer_1.Add(self.hide_votes, 0, wx.ALL, 5)
|
||||||
btn_sizer = wx.StdDialogButtonSizer()
|
btn_sizer = wx.StdDialogButtonSizer()
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class ShowUserProfile(wx.Dialog):
|
|||||||
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
|
mainSizer.Add(privateSizer, 0, wx.ALL | wx.CENTER)
|
||||||
|
|
||||||
botSizer = wx.BoxSizer(wx.HORIZONTAL)
|
botSizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
botLabel = wx.StaticText(self.panel, label=_("&Bot account: "))
|
botLabel = wx.StaticText(self.panel, label=_("B&ot account: "))
|
||||||
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
|
botText = self.createTextCtrl(bullSwitch[user.bot], (30, 30))
|
||||||
botSizer.Add(botLabel, wx.SizerFlags().Center())
|
botSizer.Add(botLabel, wx.SizerFlags().Center())
|
||||||
botSizer.Add(botText, wx.SizerFlags().Center())
|
botSizer.Add(botText, wx.SizerFlags().Center())
|
||||||
@@ -154,7 +154,7 @@ class ShowUserProfile(wx.Dialog):
|
|||||||
discoverSizer.Add(discoverText, wx.SizerFlags().Center())
|
discoverSizer.Add(discoverText, wx.SizerFlags().Center())
|
||||||
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
|
mainSizer.Add(discoverSizer, 0, wx.ALL | wx.CENTER)
|
||||||
|
|
||||||
posts = wx.Button(self.panel, label=_("{} p&osts. Click to open posts timeline").format(user.statuses_count))
|
posts = wx.Button(self.panel, label=_("{} pos&ts. Click to open posts timeline").format(user.statuses_count))
|
||||||
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
|
# posts.SetToolTip(_("Click to open {}'s posts").format(user.display_name))
|
||||||
posts.Bind(wx.EVT_BUTTON, self.onPost)
|
posts.Bind(wx.EVT_BUTTON, self.onPost)
|
||||||
mainSizer.Add(posts, wx.SizerFlags().Center())
|
mainSizer.Add(posts, wx.SizerFlags().Center())
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class UpdateProfileDialog(wx.Dialog):
|
|||||||
|
|
||||||
self.locked = wx.CheckBox(panel, label=_("&Private account"))
|
self.locked = wx.CheckBox(panel, label=_("&Private account"))
|
||||||
self.locked.SetValue(locked)
|
self.locked.SetValue(locked)
|
||||||
self.bot = wx.CheckBox(panel, label=_("&Bot account"))
|
self.bot = wx.CheckBox(panel, label=_("B&ot account"))
|
||||||
self.bot.SetValue(bot)
|
self.bot.SetValue(bot)
|
||||||
self.discoverable = wx.CheckBox(panel, label=_("&Discoverable account"))
|
self.discoverable = wx.CheckBox(panel, label=_("&Discoverable account"))
|
||||||
self.discoverable.SetValue(discoverable)
|
self.discoverable.SetValue(discoverable)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class EditTemplateDialog(wx.Dialog):
|
|||||||
sizer_3.AddButton(self.button_SAVE)
|
sizer_3.AddButton(self.button_SAVE)
|
||||||
self.button_CANCEL = wx.Button(self, wx.ID_CANCEL)
|
self.button_CANCEL = wx.Button(self, wx.ID_CANCEL)
|
||||||
sizer_3.AddButton(self.button_CANCEL)
|
sizer_3.AddButton(self.button_CANCEL)
|
||||||
self.button_RESTORE = wx.Button(self, wx.ID_ANY, _("Restore template"))
|
self.button_RESTORE = wx.Button(self, wx.ID_ANY, _("&Restore template"))
|
||||||
self.button_RESTORE.Bind(wx.EVT_BUTTON, self.on_restore)
|
self.button_RESTORE.Bind(wx.EVT_BUTTON, self.on_restore)
|
||||||
sizer_3.AddButton(self.button_CANCEL)
|
sizer_3.AddButton(self.button_CANCEL)
|
||||||
sizer_3.Realize()
|
sizer_3.Realize()
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ class UserListDialog(wx.Dialog):
|
|||||||
user_list_sizer.Add(self.user_list, 1, wx.EXPAND | wx.ALL, 10)
|
user_list_sizer.Add(self.user_list, 1, wx.EXPAND | wx.ALL, 10)
|
||||||
main_sizer.Add(user_list_sizer, 1, wx.EXPAND | wx.ALL, 15)
|
main_sizer.Add(user_list_sizer, 1, wx.EXPAND | wx.ALL, 15)
|
||||||
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
self.actions_button = wx.Button(panel, wx.ID_ANY, "Actions")
|
self.actions_button = wx.Button(panel, wx.ID_ANY, "&Actions")
|
||||||
buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10)
|
buttons_sizer.Add(self.actions_button, 0, wx.RIGHT, 10)
|
||||||
self.details_button = wx.Button(panel, wx.ID_ANY, _("View profile"))
|
self.details_button = wx.Button(panel, wx.ID_ANY, _("&View profile"))
|
||||||
buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10)
|
buttons_sizer.Add(self.details_button, 0, wx.RIGHT, 10)
|
||||||
close_button = wx.Button(panel, wx.ID_CANCEL, "Close")
|
close_button = wx.Button(panel, wx.ID_CANCEL, "&Close")
|
||||||
buttons_sizer.Add(close_button, 0)
|
buttons_sizer.Add(close_button, 0)
|
||||||
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
|
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
|
||||||
panel.SetSizer(main_sizer)
|
panel.SetSizer(main_sizer)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class mainFrame(wx.Frame):
|
|||||||
self.menuitem_search = self.menubar_application.Append(wx.ID_ANY, _(u"&Search"))
|
self.menuitem_search = self.menubar_application.Append(wx.ID_ANY, _(u"&Search"))
|
||||||
self.lists = self.menubar_application.Append(wx.ID_ANY, _(u"&Lists manager"))
|
self.lists = self.menubar_application.Append(wx.ID_ANY, _(u"&Lists manager"))
|
||||||
self.lists.Enable(False)
|
self.lists.Enable(False)
|
||||||
self.manageAliases = self.menubar_application.Append(wx.ID_ANY, _("Manage user aliases"))
|
self.manageAliases = self.menubar_application.Append(wx.ID_ANY, _("M&anage user aliases"))
|
||||||
self.keystroke_editor = self.menubar_application.Append(wx.ID_ANY, _(u"&Edit keystrokes"))
|
self.keystroke_editor = self.menubar_application.Append(wx.ID_ANY, _(u"&Edit keystrokes"))
|
||||||
self.account_settings = self.menubar_application.Append(wx.ID_ANY, _(u"Account se&ttings"))
|
self.account_settings = self.menubar_application.Append(wx.ID_ANY, _(u"Account se&ttings"))
|
||||||
self.prefs = self.menubar_application.Append(wx.ID_PREFERENCES, _(u"&Global settings"))
|
self.prefs = self.menubar_application.Append(wx.ID_PREFERENCES, _(u"&Global settings"))
|
||||||
@@ -56,7 +56,7 @@ class mainFrame(wx.Frame):
|
|||||||
self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
|
self.trends = self.menubar_buffer.Append(wx.ID_ANY, _(u"New &trending topics buffer..."))
|
||||||
self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
|
self.filter = self.menubar_buffer.Append(wx.ID_ANY, _(u"Create a &filter"))
|
||||||
self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters"))
|
self.manage_filters = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Manage filters"))
|
||||||
self.find = self.menubar_buffer.Append(wx.ID_ANY, _(u"Find a string in the currently focused buffer..."))
|
self.find = self.menubar_buffer.Append(wx.ID_ANY, _(u"F&ind a string in the currently focused buffer..."))
|
||||||
self.load_previous_items = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Load previous items"))
|
self.load_previous_items = self.menubar_buffer.Append(wx.ID_ANY, _(u"&Load previous items"))
|
||||||
self.menubar_buffer.AppendSeparator()
|
self.menubar_buffer.AppendSeparator()
|
||||||
self.mute_buffer = self.menubar_buffer.AppendCheckItem(wx.ID_ANY, _(u"&Mute"))
|
self.mute_buffer = self.menubar_buffer.AppendCheckItem(wx.ID_ANY, _(u"&Mute"))
|
||||||
@@ -66,8 +66,8 @@ class mainFrame(wx.Frame):
|
|||||||
|
|
||||||
# audio menu
|
# audio menu
|
||||||
self.menubar_audio = wx.Menu()
|
self.menubar_audio = wx.Menu()
|
||||||
self.seekLeft = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek back 5 seconds"))
|
self.seekLeft = self.menubar_audio.Append(wx.ID_ANY, _(u"Seek &back 5 seconds"))
|
||||||
self.seekRight = self.menubar_audio.Append(wx.ID_ANY, _(u"&Seek forward 5 seconds"))
|
self.seekRight = self.menubar_audio.Append(wx.ID_ANY, _(u"Seek &forward 5 seconds"))
|
||||||
|
|
||||||
# Help Menu
|
# Help Menu
|
||||||
self.menubar_help = wx.Menu()
|
self.menubar_help = wx.Menu()
|
||||||
@@ -134,9 +134,9 @@ class mainFrame(wx.Frame):
|
|||||||
self.buffers[name] = buffer.GetId()
|
self.buffers[name] = buffer.GetId()
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
self.sizer.Add(self.nb, 0, wx.ALL, 5)
|
self.sizer.Add(self.nb, 1, wx.ALL | wx.EXPAND, 5)
|
||||||
self.panel.SetSizer(self.sizer)
|
self.panel.SetSizer(self.sizer)
|
||||||
# self.Maximize()
|
self.Maximize()
|
||||||
self.sizer.Layout()
|
self.sizer.Layout()
|
||||||
self.SetClientSize(self.sizer.CalcMin())
|
self.SetClientSize(self.sizer.CalcMin())
|
||||||
# print self.GetSize()
|
# print self.GetSize()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{"current_version": "2025.03.08",
|
{
|
||||||
"description": "Initial filter support, added pinned posts. Fixed some minor issues.",
|
"current_version": "2025.03.08",
|
||||||
"date": "unknown",
|
"description": "Added support for editing and scheduling Mastodon posts, improved quoted posts reading, and added server announcements buffer. Includes visual stability fixes and keyboard shortcut improvements.",
|
||||||
|
"date": "2026-01-12",
|
||||||
"downloads":
|
"downloads":
|
||||||
{"Windows32": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2024.05.23.zip",
|
{
|
||||||
"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2025.03.08/TWBlue_portable_v2025.03.08.zip"}
|
"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2026.01.12/TWBlue_portable_v2026.01.12.zip"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user