mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2025-11-06 22:47:04 +00:00
Compare commits
184 Commits
features/p
...
claude/ini
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
977de1332a | ||
|
|
cbafb7da69 | ||
|
|
966d43ca29 | ||
|
|
193f9bfb66 | ||
|
|
2f77675562 | ||
|
|
b13dd986fc | ||
|
|
4269124758 | ||
|
|
6be0e82610 | ||
|
|
a6e8f2969a | ||
|
|
eb3a038a60 | ||
|
|
bb60eaa2fc | ||
|
|
46654ef054 | ||
|
|
981d98e5e2 | ||
|
|
7eda523520 | ||
|
|
ea54406127 | ||
|
|
1db78da82f | ||
|
|
9c0b7f0c78 | ||
|
|
e62b4d9d77 | ||
|
|
a99f2e9e64 | ||
|
|
0ad32886e1 | ||
|
|
b62b83fa00 | ||
|
|
bfd0d938e1 | ||
|
|
4d2d044aa9 | ||
|
|
e8e1c4dedf | ||
|
|
dd23074177 | ||
|
|
127cd5ddf3 | ||
|
|
cdeab8ffc6 | ||
|
|
d2fc571100 | ||
|
|
4a27571e04 | ||
|
|
f3296d646e | ||
|
|
8c0a863770 | ||
|
|
2b8370a377 | ||
| 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 | ||
|
|
b15151ddf3 | ||
|
|
a2312e8874 | ||
|
|
663431ef5d | ||
|
|
f3cba380ba | ||
|
|
4c8c7ed384 | ||
|
|
0395b372bd | ||
|
|
a2c5bbe65c | ||
|
|
b0ca33c9d8 | ||
|
|
350b25cc67 | ||
|
|
51cf283421 | ||
|
|
fb227886d2 | ||
|
|
0d49b11411 | ||
|
|
43af826eb0 | ||
|
|
b56379e485 | ||
|
|
f8c770947f | ||
|
|
a55dc1b4aa | ||
|
|
cd146e9cf6 | ||
|
|
ecb56ae4f0 | ||
|
|
e58543d6c4 | ||
|
|
35917ee05a | ||
|
|
411465db67 | ||
|
|
8eed4a12c9 | ||
|
|
894f3fd113 | ||
|
|
014c510061 | ||
|
|
f55ff3a3a9 | ||
|
|
67145df1b6 | ||
|
|
0b8f124088 | ||
|
|
12e80dc7e2 | ||
|
|
f58b1b03d2 | ||
|
|
8fc627b77a | ||
|
|
f99282025b | ||
|
|
04916588c3 | ||
|
|
c6501664ee | ||
|
|
f5ba3fdf14 | ||
|
|
e6be7c2563 | ||
|
|
35cba4c6c6 | ||
|
|
4e11d2562a | ||
|
|
00a97a66a4 | ||
|
|
0030b1c65d | ||
|
|
689afb0682 | ||
|
|
08316733d3 | ||
|
|
e5bdddd45c | ||
|
|
b3cd888424 | ||
|
|
f9577de904 | ||
|
|
b011879b0d | ||
|
|
6a64147d96 | ||
|
|
a45138d68b | ||
|
|
47bb008710 | ||
| a64cc0a28c | |||
|
|
a5afd1b1a8 | ||
| 487282d512 | |||
| 54d0f70f38 | |||
|
|
b78eec35f8 | ||
|
|
84b8932df9 | ||
| 128575df05 | |||
| 64af7dc02b | |||
| 6a8ef9b523 | |||
| 2a4a17fdeb | |||
|
|
bea28d8d19 | ||
|
|
89a89778aa | ||
| 2b4a66aa05 | |||
| 70a1c9fbcc | |||
| 76dae8bcf8 | |||
| b432873500 | |||
| 45bee2bf57 | |||
| 921fe631e0 |
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,8 +2,12 @@ TWBlue Changelog
|
||||
|
||||
## changes in this version
|
||||
|
||||
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:
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* TWBlue should be able to display all posts in the post displayer dialog.
|
||||
* reading long posts in the graphical user interface should work better.
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
## Changelog
|
||||
|
||||
In this version of TWBlue, which is being released several months after the previous one, we focused on adding initial support for GoToSocial-type networks. GoToSocial is a server for creating decentralized networks similar to Mastodon. Its API is very similar but retains some differences. In this version, TWBlue can be used to log into GoToSocial accounts, although there will be some features, such as the Streaming API and Markdown support, that are not yet functional. Another significant addition is support for creating community timelines, which will allow you to load the local and public timeline of remote instances. This is useful if your instance does not federate directly with them, as it will allow you to see posts from other communities and interact directly with them. Finally, the translation module has been rewritten; it now supports using LibreTranslate by default and DeepL, for which an API key is required. Below is the detailed list of changes:
|
||||
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.
|
||||
|
||||
* Core:
|
||||
* Added Initial Support to GoToSocial. Some features are not fully implemented yet, although GoToXocial instances should be able to be used as normal sessions in TWBlue. Streaming, poll options and markdown are not supported but planned for the near future.
|
||||
* The translation module has been rewritten. Now, instead of offering translations with Google Translator, the user can choose between [LibreTranslate,](https://github.com/LibreTranslate/LibreTranslate) which requires no configuration thanks to the [instance of the NVDA Spanish community;](https://translate.nvda.es) or translate using [DeepL,](https://deepl.com) for which it is necessary to create an account on DeepL and [subscribe to a DeepL API Free plan](https://support.deepl.com/hc/en-us/articles/360021200939-DeepL-API-Free) to obtain the API key which can be used to translate up to 500000 characters every month. The API key can be entered in the global options dialog, under a new tab called translation services. When translating a text, the translation engine can be changed. When changing the translation engine, the target language must be selected again before translation takes place.
|
||||
* TWBlue should be able to switch to Windows 11 Keymap when running under Windows 11. ([#494](https://github.com/mcv-software/twblue/issues/494))
|
||||
* Mastodon:
|
||||
* Added support for viewing communities: A community timeline is the local or public timeline of another instance. This is especially useful when the instance one is part of does not federate with other remote instances. The posts displayed are only those that are shared publicly. It is possible to interact with the posts from community timelines, but it should be noted that TWBlue will take some time to retrieve the post one wishes to interact with.
|
||||
* When viewing a post, a button displays the number of boosts and times it has been added to favorites. Clicking on that button will open a list of users who have interacted with the post. From that list, it is possible to view profiles and perform common user actions.
|
||||
* Now it is possible to mute conversations in Mastodon sessions. To do this, there is a button that can be called "Mute" or "Unmute Conversation" in the dialog to display the post. Conversations that have been muted will not generate notifications or mentions when they receive new replies. Only conversations that you are a part of can be muted.
|
||||
* Fixed an error that caused TWBlue to be unable to properly display the user action dialog from the followers or following buffer. ([#575](https://github.com/mcv-software/twblue/issues/575))
|
||||
* 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.
|
||||
* 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.
|
||||
* 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.
|
||||
* TWBlue should be able to display all posts in the post displayer dialog.
|
||||
* reading long posts in the graphical user interface should work better.
|
||||
|
||||
@@ -1,57 +1,58 @@
|
||||
accessible_output2 @ git+https://github.com/accessibleapps/accessible_output2@57bda997d98e87dd78aa049e7021cf777871619b
|
||||
arrow==1.3.0
|
||||
attrs==25.1.0
|
||||
arrow==1.4.0
|
||||
attrs==25.4.0
|
||||
backports.functools-lru-cache==2.0.0
|
||||
blurhash==1.1.4
|
||||
certifi==2025.1.31
|
||||
blurhash==1.1.5
|
||||
certifi==2025.10.5
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.4
|
||||
colorama==0.4.6
|
||||
configobj==5.0.9
|
||||
coverage==7.6.12
|
||||
cx-Freeze==7.2.10
|
||||
coverage==7.11.0
|
||||
cx-Freeze==8.4.1
|
||||
cx-Logging==3.2.1
|
||||
decorator==5.2.1
|
||||
demoji==1.1.0
|
||||
deepl==1.21.0
|
||||
deepl==1.23.0
|
||||
future==1.0.0
|
||||
idna==3.10
|
||||
importlib-metadata==8.6.1
|
||||
iniconfig==2.0.0
|
||||
idna==3.11
|
||||
importlib-metadata==8.7.0
|
||||
iniconfig==2.3.0
|
||||
libloader @ git+https://github.com/accessibleapps/libloader@bc94811c095b2e57a036acd88660be9a33260267
|
||||
libretranslatepy==2.1.4
|
||||
lief==0.15.1
|
||||
Markdown==3.7
|
||||
Mastodon.py==2.0.1
|
||||
numpy==2.2.3
|
||||
oauthlib==3.2.2
|
||||
packaging==24.2
|
||||
pillow==11.1.0
|
||||
Markdown==3.10
|
||||
Mastodon.py==2.1.4
|
||||
numpy==2.3.4
|
||||
oauthlib==3.3.1
|
||||
packaging==25.0
|
||||
pillow==12.0.0
|
||||
platform_utils @ git+https://github.com/accessibleapps/platform_utils@e0d79f7b399c4ea677a633d2dde9202350d62c38
|
||||
pluggy==1.5.0
|
||||
psutil==7.0.0
|
||||
pyenchant==3.2.2
|
||||
pluggy==1.6.0
|
||||
psutil==7.1.3
|
||||
pyenchant==3.3.0
|
||||
pypiwin32==223
|
||||
Pypubsub==4.0.3
|
||||
PySocks==1.7.1
|
||||
pytest==8.3.5
|
||||
pytest==8.4.2
|
||||
python-dateutil==2.9.0.post0
|
||||
python-magic-bin==0.4.14
|
||||
python-vlc==3.0.21203
|
||||
pywin32==308
|
||||
requests==2.32.3
|
||||
pywin32==311
|
||||
requests==2.32.5
|
||||
requests-oauthlib==2.0.0
|
||||
requests-toolbelt==1.0.0
|
||||
rfc3986==2.0.0
|
||||
setuptools==69.0.0
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
sound_lib @ git+https://github.com/accessibleapps/sound_lib@a439f0943fb95ee7b6ba24f51a686f47c4ad66b2
|
||||
sqlitedict==2.1.0
|
||||
twitter-text-parser==3.0.0
|
||||
types-python-dateutil==2.9.0.20241206
|
||||
urllib3==2.3.0
|
||||
types-python-dateutil==2.9.0.20251008
|
||||
urllib3==2.5.0
|
||||
win-inet-pton==1.1.0
|
||||
winpaths==0.2
|
||||
wxPython==4.2.2
|
||||
wxPython==4.2.4
|
||||
youtube-dl==2021.12.17
|
||||
zipp==3.21.0
|
||||
zipp==3.23.0
|
||||
@@ -280,6 +280,12 @@ class BaseBuffer(base.Buffer):
|
||||
return
|
||||
menu = menus.base()
|
||||
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)
|
||||
if self.can_share() == True:
|
||||
widgetUtils.connect_event(menu, widgetUtils.MENU, self.share_item, menuitem=menu.boost)
|
||||
@@ -501,6 +507,25 @@ class BaseBuffer(base.Buffer):
|
||||
log.exception("")
|
||||
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
|
||||
# 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
|
||||
call_threaded(self.session.edit_post, post_id=post.post_id, posts=post_data, visibility=post.get_visibility(), language=post.get_language())
|
||||
if hasattr(post.message, "destroy"):
|
||||
post.message.destroy()
|
||||
|
||||
def user_details(self):
|
||||
item = self.get_item()
|
||||
pass
|
||||
@@ -572,7 +597,6 @@ class BaseBuffer(base.Buffer):
|
||||
except MastodonNotFoundError:
|
||||
output.speak(_("No status found with that ID"))
|
||||
return
|
||||
# print(item)
|
||||
msg = messages.viewPost(self.session, item, offset_hours=self.session.db["utc_offset"], item_url=self.get_item_url(item=item))
|
||||
|
||||
def ocr_image(self):
|
||||
|
||||
@@ -161,6 +161,13 @@ class NotificationsBuffer(BaseBuffer):
|
||||
menu = menus.notification(notification.type)
|
||||
if self.is_post():
|
||||
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)
|
||||
if self.can_share() == True:
|
||||
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.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):
|
||||
if config.app["app-settings"]["ask_at_exit"] == True:
|
||||
answer = commonMessageDialogs.exit_dialog(self.view)
|
||||
|
||||
@@ -254,7 +254,6 @@ class post(messages.basicMessage):
|
||||
langs = self.session.supported_languages
|
||||
lang = self.message.language.GetSelection()
|
||||
if lang >= 0:
|
||||
print(langs[lang].code)
|
||||
return langs[lang].code
|
||||
return None
|
||||
|
||||
@@ -263,6 +262,47 @@ class post(messages.basicMessage):
|
||||
visibility_setting = visibility_settings.index(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. """
|
||||
# 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
|
||||
visibility_settings = dict(public=0, unlisted=1, private=2, direct=3)
|
||||
self.message.visibility.SetSelection(visibility_settings.get(item.visibility, 0))
|
||||
# Set language
|
||||
if item.language:
|
||||
self.set_language(item.language)
|
||||
# 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 media attachments
|
||||
if hasattr(item, 'media_attachments') and len(item.media_attachments) > 0:
|
||||
for media in item.media_attachments:
|
||||
media_info = {
|
||||
"id": media.id, # Keep the existing media ID
|
||||
"type": media.type,
|
||||
"file": media.url, # URL of existing media
|
||||
"description": media.description or ""
|
||||
}
|
||||
self.attachments.append(media_info)
|
||||
# Display in the attachment list
|
||||
self.message.add_item(item=[media.url.split('/')[-1], media.type, media.description or ""])
|
||||
# Update text processor to reflect the loaded content
|
||||
self.text_processor()
|
||||
|
||||
class viewPost(post):
|
||||
def __init__(self, session, post, offset_hours=0, date="", item_url=""):
|
||||
self.session = session
|
||||
|
||||
@@ -23,6 +23,7 @@ url = string(default="control+win+b")
|
||||
go_home = string(default="control+win+home")
|
||||
go_end = string(default="control+win+end")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
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")
|
||||
update_profile = string(default="control+win+shift+p")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
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")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="alt+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="alt+win+shift+delete")
|
||||
repeat_item = string(default="alt+win+space")
|
||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||
|
||||
@@ -33,6 +33,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="alt+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="alt+win+shift+delete")
|
||||
repeat_item = string(default="control+alt+win+space")
|
||||
copy_to_clipboard = string(default="alt+win+shift+c")
|
||||
|
||||
@@ -34,6 +34,7 @@ go_page_up = string(default="control+win+pageup")
|
||||
go_page_down = string(default="control+win+pagedown")
|
||||
update_profile = string(default="alt+win+p")
|
||||
delete = string(default="control+win+delete")
|
||||
edit_post = string(default="")
|
||||
clear_buffer = string(default="control+win+shift+delete")
|
||||
repeat_item = string(default="control+win+space")
|
||||
copy_to_clipboard = string(default="control+win+shift+c")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
src/locales/bg/LC_MESSAGES/twblue.mo
Normal file
BIN
src/locales/bg/LC_MESSAGES/twblue.mo
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -248,6 +248,36 @@ class Session(base.baseSession):
|
||||
pub.sendMessage("mastodon.error_post", name=self.get_name(), reply_to=reply_to, visibility=visibility, posts=posts, lang=language)
|
||||
return
|
||||
|
||||
def edit_post(self, post_id, visibility=None, language=None, posts=[]):
|
||||
""" Convenience function to edit a post. Only the first item in posts list is used as threads cannot be edited. """
|
||||
if len(posts) == 0:
|
||||
return
|
||||
obj = posts[0]
|
||||
text = obj.get("text")
|
||||
media_ids = []
|
||||
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"])
|
||||
# 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"])
|
||||
# Otherwise it's a new file to upload
|
||||
elif "file" in i:
|
||||
media = self.api_call("media_post", media_file=i["file"], description=i["description"], synchronous=True)
|
||||
media_ids.append(media.id)
|
||||
# Call status_update API
|
||||
item = self.api_call(call_name="status_update", id=post_id, status=text, _sound="tweet_send.ogg", media_ids=media_ids if len(media_ids) > 0 else None, visibility=visibility, poll=poll, sensitive=obj["sensitive"], spoiler_text=obj["spoiler_text"], language=language)
|
||||
return item
|
||||
except Exception as e:
|
||||
log.exception("Error updating post: {}".format(str(e)))
|
||||
output.speak(_("Error editing post: {}").format(str(e)))
|
||||
return None
|
||||
|
||||
def get_name(self):
|
||||
instance = self.settings["mastodon"]["instance"]
|
||||
instance = instance.replace("https://", "")
|
||||
|
||||
@@ -8,6 +8,8 @@ class base(wx.Menu):
|
||||
self.Append(self.boost)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||
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.Append(self.fav)
|
||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||
@@ -36,6 +38,8 @@ class notification(wx.Menu):
|
||||
self.Append(self.boost)
|
||||
self.reply = wx.MenuItem(self, wx.ID_ANY, _(u"Re&ply"))
|
||||
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.Append(self.fav)
|
||||
self.unfav = wx.MenuItem(self, wx.ID_ANY, _(u"R&emove from favorites"))
|
||||
|
||||
@@ -51,7 +51,7 @@ class Post(wx.Dialog):
|
||||
visibility_sizer.Add(self.visibility, 0, 0, 0)
|
||||
language_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
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)
|
||||
@@ -234,9 +234,9 @@ class viewPost(wx.Dialog):
|
||||
|
||||
def create_buttons_section(self, panel):
|
||||
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.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.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
||||
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
||||
@@ -295,7 +295,7 @@ class poll(wx.Dialog):
|
||||
sizer_1 = wx.BoxSizer(wx.VERTICAL)
|
||||
period_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
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()
|
||||
@@ -305,36 +305,36 @@ class poll(wx.Dialog):
|
||||
sizer_1.Add(sizer_2, 1, wx.EXPAND, 0)
|
||||
option1_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
self.option1 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option1.SetMaxLength(25)
|
||||
option1_sizer.Add(self.option1, 0, 0, 0)
|
||||
option2_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
self.option2 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option2.SetMaxLength(25)
|
||||
option2_sizer.Add(self.option2, 0, 0, 0)
|
||||
option3_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
self.option3 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option3.SetMaxLength(25)
|
||||
option3_sizer.Add(self.option3, 0, 0, 0)
|
||||
option4_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
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)
|
||||
self.option4 = wx.TextCtrl(self, wx.ID_ANY, "")
|
||||
self.option4.SetMaxLength(25)
|
||||
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)
|
||||
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)
|
||||
sizer_1.Add(self.hide_votes, 0, wx.ALL, 5)
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
|
||||
@@ -26,7 +26,7 @@ class EditTemplateDialog(wx.Dialog):
|
||||
sizer_3.AddButton(self.button_SAVE)
|
||||
self.button_CANCEL = wx.Button(self, wx.ID_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)
|
||||
sizer_3.AddButton(self.button_CANCEL)
|
||||
sizer_3.Realize()
|
||||
|
||||
@@ -22,11 +22,11 @@ class UserListDialog(wx.Dialog):
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
close_button = wx.Button(panel, wx.ID_CANCEL, "Close")
|
||||
close_button = wx.Button(panel, wx.ID_CANCEL, "&Close")
|
||||
buttons_sizer.Add(close_button, 0)
|
||||
main_sizer.Add(buttons_sizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 15)
|
||||
panel.SetSizer(main_sizer)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{"current_version": "2024.05.23",
|
||||
"description": "Read long posts in GUI, display posts with unicode characters and several buffixes.",
|
||||
{"current_version": "2025.03.08",
|
||||
"description": "Initial filter support, added pinned posts. Fixed some minor issues.",
|
||||
"date": "unknown",
|
||||
"downloads":
|
||||
{"Windows32": "https://github.com/MCV-Software/TWBlue/releases/download/v2024.05.23/TWBlue_portable_v2024.05.23.zip",
|
||||
"Windows64": "https://github.com/MCV-Software/TWBlue/releases/download/v2024.05.23/TWBlue_portable_v2024.05.23.zip"}
|
||||
{"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"}
|
||||
}
|
||||
Reference in New Issue
Block a user