🔧 chore: update code
This commit is contained in:
19 files changed
+2336
-393
No files matched your search
@@ -18,6 +18,7 @@ from .battery import Battery
|
||||
from .controls import Controls
|
||||
from .indicators import Indicators
|
||||
from .metrics import Metrics
|
||||
from .music_player import MusicPlayer
|
||||
from .workspaces import workspace
|
||||
|
||||
import gi
|
||||
@@ -110,6 +111,7 @@ class DockComponents(Box):
|
||||
)
|
||||
self.controls = Controls()
|
||||
self.indicators = Indicators()
|
||||
self.music_player = MusicPlayer()
|
||||
self.systray = SystemTray(
|
||||
icon_size=18,
|
||||
spacing=4,
|
||||
@@ -122,6 +124,7 @@ class DockComponents(Box):
|
||||
self.workspaces,
|
||||
self.metrics,
|
||||
self.controls,
|
||||
self.music_player,
|
||||
self.applications,
|
||||
self.indicators,
|
||||
self.battery,
|
||||
@@ -155,6 +158,7 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
"language": self.language,
|
||||
@@ -186,6 +190,7 @@ class DockComponents(Box):
|
||||
"date_time": self.date_time,
|
||||
"controls": self.controls,
|
||||
"indicators": self.indicators,
|
||||
"music_player": self.music_player,
|
||||
"systray": self.systray,
|
||||
"applications": self.applications,
|
||||
"language": self.language,
|
||||
|
||||
@@ -0,0 +1,333 @@
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.circularprogressbar import CircularProgressBar
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
from gi.repository import Gdk
|
||||
|
||||
import config.data as data
|
||||
import utils.icons as icons
|
||||
from services.mpris import PlayerManager, PlayerService
|
||||
from utils.wayland import WaylandWindow as Window
|
||||
from player import PlayerContainer
|
||||
|
||||
|
||||
class MusicPlayerPopup(Window):
|
||||
"""Popup window that shows the full music player interface"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Get dock position for proper anchoring
|
||||
dock_position = data.DOCK_POSITION
|
||||
popup_anchor = self._get_popup_anchor(dock_position)
|
||||
margin = self._get_popup_margin(dock_position)
|
||||
|
||||
super().__init__(
|
||||
name="music-player-popup",
|
||||
layer="top",
|
||||
anchor=popup_anchor,
|
||||
margin=margin,
|
||||
exclusive=False,
|
||||
keyboard_mode="on-demand",
|
||||
visible=False,
|
||||
all_visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.player_container = PlayerContainer()
|
||||
self.add(self.player_container)
|
||||
|
||||
# Connect to escape key to close
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
self.connect("button-press-event", self.on_button_press)
|
||||
self.set_can_focus(True)
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
"""Handle key press events"""
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.hide_popup()
|
||||
return True
|
||||
return False
|
||||
|
||||
def show_popup(self):
|
||||
"""Show the music player popup"""
|
||||
self.set_visible(True)
|
||||
self.grab_focus()
|
||||
|
||||
def _get_popup_anchor(self, dock_position):
|
||||
"""Get popup anchor based on dock position"""
|
||||
anchor_map = {
|
||||
"Top": "top",
|
||||
"Bottom": "bottom",
|
||||
"Left": "left",
|
||||
"Right": "right",
|
||||
}
|
||||
return anchor_map.get(dock_position, "bottom")
|
||||
|
||||
def _get_popup_margin(self, dock_position):
|
||||
"""Get popup margin based on dock position"""
|
||||
margin_map = {
|
||||
"Top": "60px 10px 10px 10px",
|
||||
"Bottom": "10px 10px 60px 10px",
|
||||
"Left": "10px 10px 10px 60px",
|
||||
"Right": "10px 60px 10px 10px",
|
||||
}
|
||||
return margin_map.get(dock_position, "10px 10px 60px 10px")
|
||||
|
||||
def on_button_press(self, widget, event):
|
||||
"""Handle button press events"""
|
||||
return False
|
||||
|
||||
def hide_popup(self):
|
||||
"""Hide the music player popup"""
|
||||
self.set_visible(False)
|
||||
|
||||
|
||||
class MusicPlayer(Box):
|
||||
"""Music player component for the dock with circular progress bar"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="music-player", **kwargs)
|
||||
|
||||
# Initialize player manager
|
||||
self.manager = PlayerManager()
|
||||
self.manager.connect("new-player", self.on_new_player)
|
||||
self.manager.connect("player-vanish", self.on_player_vanish)
|
||||
|
||||
# Current player tracking
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
self.duration = 0.0
|
||||
self.position = 0.0
|
||||
self._current_track_info = "Music Player"
|
||||
|
||||
# Create circular progress bar
|
||||
self.progress_bar = CircularProgressBar(
|
||||
name="player-circle",
|
||||
size=28,
|
||||
line_width=2,
|
||||
start_angle=150,
|
||||
end_angle=390,
|
||||
value=0.0, # Initialize with 0 progress
|
||||
)
|
||||
|
||||
# Create music icon label - always use generic music icon
|
||||
self.music_label = Label(name="music-label", markup=icons.music)
|
||||
|
||||
# Create button for the music icon
|
||||
self.music_button = Button(
|
||||
on_clicked=self.on_music_clicked, child=self.music_label
|
||||
)
|
||||
|
||||
# Create event box for scroll events (volume control)
|
||||
self.event_box = EventBox(
|
||||
events=["scroll", "smooth-scroll"],
|
||||
child=Overlay(child=self.progress_bar, overlays=self.music_button),
|
||||
)
|
||||
|
||||
# Connect scroll events for seeking
|
||||
self.event_box.connect("scroll-event", self.on_scroll)
|
||||
self.add_events(Gdk.EventMask.SCROLL_MASK | Gdk.EventMask.SMOOTH_SCROLL_MASK)
|
||||
|
||||
self.add(self.event_box)
|
||||
|
||||
# Popup window will be created on-demand
|
||||
self.popup = None
|
||||
|
||||
# Initialize players
|
||||
self.manager.init_all_players()
|
||||
|
||||
# Set initial visibility
|
||||
self.update_visibility()
|
||||
|
||||
# Connect to destroy signal for cleanup
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Clean up popup when component is destroyed"""
|
||||
if self.popup:
|
||||
self.popup.destroy()
|
||||
self.popup = None
|
||||
|
||||
def on_new_player(self, manager, player):
|
||||
"""Handle new player detection"""
|
||||
print(f"Music dock component: New player detected - {player.props.player_name}")
|
||||
|
||||
# If this is our first player, make it current
|
||||
if not self.current_player:
|
||||
self.set_current_player(player)
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
def on_player_vanish(self, manager, player):
|
||||
"""Handle player disappearing"""
|
||||
print(f"Music dock component: Player vanished - {player.props.player_name}")
|
||||
|
||||
# If the current player vanished, find another one
|
||||
if (
|
||||
self.current_player
|
||||
and self.current_player.props.player_name == player.props.player_name
|
||||
):
|
||||
self.current_player = None
|
||||
self.current_player_service = None
|
||||
|
||||
# Try to find another active player
|
||||
# This is a simple approach - in a real implementation you might want more sophisticated logic
|
||||
|
||||
self.update_visibility()
|
||||
|
||||
def set_current_player(self, player):
|
||||
"""Set the current active player"""
|
||||
# Disconnect from previous player
|
||||
if self.current_player_service:
|
||||
try:
|
||||
self.current_player_service.disconnect_by_func(self.on_track_position)
|
||||
self.current_player_service.disconnect_by_func(self.on_play)
|
||||
self.current_player_service.disconnect_by_func(self.on_pause)
|
||||
self.current_player_service.disconnect_by_func(self.on_metadata)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.current_player = player
|
||||
self.current_player_service = PlayerService(player=player)
|
||||
|
||||
# Connect to player events
|
||||
self.current_player_service.connect("track-position", self.on_track_position)
|
||||
self.current_player_service.connect("play", self.on_play)
|
||||
self.current_player_service.connect("pause", self.on_pause)
|
||||
self.current_player_service.connect("meta-change", self.on_metadata)
|
||||
|
||||
# Always use generic music icon, not player-specific icons
|
||||
self.music_label.set_markup(icons.music)
|
||||
|
||||
# Update initial state
|
||||
self.update_playback_state()
|
||||
|
||||
def on_track_position(self, service, pos, dur):
|
||||
"""Handle track position updates"""
|
||||
self.position = pos
|
||||
self.duration = dur
|
||||
|
||||
if dur > 0:
|
||||
progress = pos / dur
|
||||
# Use value property instead of set_percentage for CircularProgressBar
|
||||
self.progress_bar.value = progress
|
||||
|
||||
# Update tooltip with timestamp info
|
||||
pos_min = int(pos // 60)
|
||||
pos_sec = int(pos % 60)
|
||||
dur_min = int(dur // 60)
|
||||
dur_sec = int(dur % 60)
|
||||
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
|
||||
|
||||
# Get current track info if available
|
||||
if hasattr(self, '_current_track_info'):
|
||||
tooltip = f"{self._current_track_info}\n{timestamp_text}"
|
||||
else:
|
||||
tooltip = f"Music Player\n{timestamp_text}"
|
||||
self.event_box.set_tooltip_text(tooltip)
|
||||
else:
|
||||
self.progress_bar.value = 0
|
||||
self.event_box.set_tooltip_text("Music Player")
|
||||
|
||||
def on_play(self, service):
|
||||
"""Handle play state"""
|
||||
self.music_label.set_markup(icons.music)
|
||||
self.progress_bar.remove_style_class("paused")
|
||||
# Update tooltip to show playing state
|
||||
if hasattr(self, '_current_track_info'):
|
||||
self.event_box.set_tooltip_text(f"{self._current_track_info} (Playing)")
|
||||
|
||||
def on_pause(self, service):
|
||||
"""Handle pause state"""
|
||||
# Always use generic music icon when paused
|
||||
self.music_label.set_markup(icons.music)
|
||||
self.progress_bar.add_style_class("paused")
|
||||
# Update tooltip to show paused state
|
||||
if hasattr(self, '_current_track_info'):
|
||||
self.event_box.set_tooltip_text(f"{self._current_track_info} (Paused)")
|
||||
|
||||
def on_metadata(self, service, metadata, player):
|
||||
"""Handle metadata changes"""
|
||||
# Update tooltip with current track info
|
||||
keys = metadata.keys()
|
||||
if "xesam:artist" in keys and "xesam:title" in keys:
|
||||
artist = (
|
||||
metadata["xesam:artist"][0]
|
||||
if metadata["xesam:artist"]
|
||||
else "Unknown Artist"
|
||||
)
|
||||
title = metadata["xesam:title"]
|
||||
self._current_track_info = f"{artist} - {title}"
|
||||
|
||||
# If we have position info, include timestamp
|
||||
if hasattr(self, 'position') and hasattr(self, 'duration') and self.duration > 0:
|
||||
pos_min = int(self.position // 60)
|
||||
pos_sec = int(self.position % 60)
|
||||
dur_min = int(self.duration // 60)
|
||||
dur_sec = int(self.duration % 60)
|
||||
timestamp_text = f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}"
|
||||
tooltip = f"{self._current_track_info}\n{timestamp_text}"
|
||||
else:
|
||||
tooltip = self._current_track_info
|
||||
|
||||
self.event_box.set_tooltip_text(tooltip)
|
||||
else:
|
||||
self._current_track_info = "Music Player"
|
||||
self.event_box.set_tooltip_text("Music Player")
|
||||
|
||||
def update_playback_state(self):
|
||||
"""Update the visual state based on current playback"""
|
||||
if not self.current_player:
|
||||
return
|
||||
|
||||
if (
|
||||
self.current_player.props.playback_status.value_name
|
||||
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
|
||||
):
|
||||
self.on_play(None)
|
||||
else:
|
||||
self.on_pause(None)
|
||||
|
||||
def on_music_clicked(self, button):
|
||||
"""Handle music button click - show popup"""
|
||||
if self.popup and self.popup.get_visible():
|
||||
self.popup.hide_popup()
|
||||
else:
|
||||
# Create popup on-demand if it doesn't exist
|
||||
if not self.popup:
|
||||
self.popup = MusicPlayerPopup()
|
||||
self.popup.show_popup()
|
||||
|
||||
def on_scroll(self, widget, event):
|
||||
"""Handle scroll events for seeking"""
|
||||
if not self.current_player or self.duration <= 0:
|
||||
return False
|
||||
|
||||
# Calculate seek amount (5 seconds per scroll)
|
||||
seek_amount = 5.0
|
||||
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
new_position = min(self.position + seek_amount, self.duration)
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
new_position = max(self.position - seek_amount, 0)
|
||||
else:
|
||||
return False
|
||||
|
||||
# Seek to new position
|
||||
if self.current_player_service:
|
||||
self.current_player_service.set_position(int(new_position))
|
||||
|
||||
return True
|
||||
|
||||
def update_visibility(self):
|
||||
"""Update component visibility based on available players"""
|
||||
# Show component only if there are active players
|
||||
has_players = self.current_player is not None
|
||||
self.set_visible(has_players)
|
||||
|
||||
# Hide in vertical mode if user preference is set
|
||||
if data.VERTICAL and hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
|
||||
music_visible = data.DOCK_COMPONENTS_VISIBILITY.get("music_player", True)
|
||||
if not music_visible:
|
||||
self.set_visible(False)
|
||||
+149
-208
@@ -1,4 +1,3 @@
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
import threading
|
||||
@@ -11,13 +10,13 @@ from fabric.utils import get_relative_path
|
||||
import utils.icons as icons
|
||||
from modules.launcher.plugin_base import PluginBase
|
||||
from modules.launcher.result import Result
|
||||
|
||||
try:
|
||||
import pyotp
|
||||
|
||||
PYOTP_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYOTP_AVAILABLE = False
|
||||
from services.auth import (
|
||||
generate_totp,
|
||||
get_time_remaining_with_blink,
|
||||
validate_base32_secret,
|
||||
parse_otpauth_uri,
|
||||
scan_qr_and_add_account,
|
||||
)
|
||||
|
||||
|
||||
class OTPPlugin(PluginBase):
|
||||
@@ -40,11 +39,6 @@ class OTPPlugin(PluginBase):
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the OTP plugin."""
|
||||
if not PYOTP_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"pyotp is required for OTP functionality. Install with: pip install pyotp"
|
||||
)
|
||||
|
||||
self.set_triggers(["otp"])
|
||||
self._load_secrets()
|
||||
self._ensure_config_file()
|
||||
@@ -301,26 +295,11 @@ class OTPPlugin(PluginBase):
|
||||
|
||||
def _generate_totp(self, secret: str) -> Optional[str]:
|
||||
"""Generate TOTP code from secret."""
|
||||
try:
|
||||
return pyotp.TOTP(secret).now()
|
||||
except Exception as e:
|
||||
print(f"Error generating TOTP: {e}")
|
||||
return None
|
||||
|
||||
def _get_time_remaining(self) -> int:
|
||||
"""Get seconds remaining until next token refresh."""
|
||||
return 30 - (int(time.time()) % 30)
|
||||
return generate_totp(secret)
|
||||
|
||||
def _get_time_remaining_with_blink(self) -> str:
|
||||
"""Get time remaining with blinking effect."""
|
||||
time_remaining = self._get_time_remaining()
|
||||
current_second = int(time.time())
|
||||
should_blink = current_second % 2 == 0
|
||||
|
||||
if should_blink:
|
||||
return f"<span alpha='30%'>{time_remaining}s</span>"
|
||||
else:
|
||||
return f"{time_remaining}s"
|
||||
return get_time_remaining_with_blink()
|
||||
|
||||
def query(self, query_string: str) -> List[Result]:
|
||||
"""Process OTP queries."""
|
||||
@@ -357,6 +336,13 @@ class OTPPlugin(PluginBase):
|
||||
else:
|
||||
remove_content = query[7:].strip()
|
||||
return self._handle_remove_command(remove_content)
|
||||
elif query_lower == "qr" or query_lower.startswith("qr "):
|
||||
# Handle QR scanning command
|
||||
if query_lower == "qr":
|
||||
qr_content = ""
|
||||
else:
|
||||
qr_content = query[3:].strip()
|
||||
return self._handle_qr_command(qr_content)
|
||||
else:
|
||||
return self._search_accounts(query)
|
||||
|
||||
@@ -413,7 +399,7 @@ class OTPPlugin(PluginBase):
|
||||
results.append(
|
||||
Result(
|
||||
title="Available commands:",
|
||||
subtitle="add <account> <secret> | remove <account>",
|
||||
subtitle="add <account> <secret> | remove <account> | qr <account>",
|
||||
icon_markup=icons.info,
|
||||
action=lambda: None,
|
||||
relevance=0.9,
|
||||
@@ -435,7 +421,7 @@ class OTPPlugin(PluginBase):
|
||||
results.append(
|
||||
Result(
|
||||
title=f"{totp_code}",
|
||||
subtitle_markup=f"{display_name} • {time_display} remaining",
|
||||
subtitle_markup=f"{display_name} • {time_display} remaining • Shift+Enter: remove",
|
||||
icon_markup=icons.key,
|
||||
action=lambda code=totp_code: self._copy_to_clipboard(code),
|
||||
relevance=1.0,
|
||||
@@ -444,6 +430,7 @@ class OTPPlugin(PluginBase):
|
||||
"type": "totp",
|
||||
"account": account_name,
|
||||
"code": totp_code,
|
||||
"alt_action": lambda acc=account_name: self._remove_account_and_refresh(acc),
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -492,7 +479,7 @@ class OTPPlugin(PluginBase):
|
||||
title=f"{totp_code}",
|
||||
subtitle_markup=f"{display_name} • {
|
||||
time_display
|
||||
} remaining",
|
||||
} remaining • Shift+Enter: remove",
|
||||
icon_markup=icons.key,
|
||||
action=lambda code=totp_code: self._copy_to_clipboard(code),
|
||||
relevance=1.0,
|
||||
@@ -501,6 +488,7 @@ class OTPPlugin(PluginBase):
|
||||
"type": "totp",
|
||||
"account": account_name,
|
||||
"code": totp_code,
|
||||
"alt_action": lambda acc=account_name: self._remove_account_and_refresh(acc),
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -520,7 +508,7 @@ class OTPPlugin(PluginBase):
|
||||
results.append(
|
||||
Result(
|
||||
title="Available commands:",
|
||||
subtitle="add <account> <secret> | remove <account>",
|
||||
subtitle="add <account> <secret> | remove <account> | qr <account>",
|
||||
icon_markup=icons.info,
|
||||
action=lambda: None,
|
||||
relevance=0.4,
|
||||
@@ -589,6 +577,66 @@ class OTPPlugin(PluginBase):
|
||||
),
|
||||
]
|
||||
|
||||
def _handle_qr_command(self, account_name: str) -> List[Result]:
|
||||
"""Handle QR scanning command."""
|
||||
if not account_name:
|
||||
return [
|
||||
Result(
|
||||
title="Scan QR Code",
|
||||
subtitle="Click to scan QR code from screen",
|
||||
icon_markup=icons.connect,
|
||||
action=lambda: self._scan_qr_and_add_account(""),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "qr_scan"},
|
||||
),
|
||||
Result(
|
||||
title="QR Scan Instructions:",
|
||||
subtitle="Use 'qr <account_name>' to specify account name",
|
||||
icon_markup=icons.info,
|
||||
action=lambda: None,
|
||||
relevance=0.9,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "help", "keep_launcher_open": True},
|
||||
),
|
||||
]
|
||||
else:
|
||||
return [
|
||||
Result(
|
||||
title=f"Scan QR Code for '{account_name}'",
|
||||
subtitle="Click to scan QR code from screen",
|
||||
icon_markup=icons.connect,
|
||||
action=lambda name=account_name: self._scan_qr_and_add_account(name),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "qr_scan"},
|
||||
),
|
||||
]
|
||||
|
||||
def _scan_qr_and_add_account(self, account_name: str):
|
||||
"""Scan QR code and add OTP account."""
|
||||
print(f"QR scan action called for account: '{account_name}'")
|
||||
print("Starting QR scan process asynchronously...")
|
||||
|
||||
# Run QR scanning asynchronously so launcher closes immediately
|
||||
import threading
|
||||
thread = threading.Thread(target=self._scan_qr_async, args=(account_name,))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def _scan_qr_async(self, account_name: str):
|
||||
"""Async QR scanning process."""
|
||||
result = scan_qr_and_add_account(account_name, str(self.secrets_file))
|
||||
|
||||
if result["success"]:
|
||||
print(result["message"])
|
||||
# Reload secrets from file
|
||||
self._load_secrets()
|
||||
# Trigger refresh to show the new account
|
||||
self._trigger_refresh()
|
||||
else:
|
||||
print(f"QR scan failed: {result['error']}")
|
||||
|
||||
def _handle_remove_command(self, account_name: str) -> List[Result]:
|
||||
"""Handle removal of OTP account."""
|
||||
if not account_name:
|
||||
@@ -636,18 +684,18 @@ class OTPPlugin(PluginBase):
|
||||
results.append(
|
||||
Result(
|
||||
title=f"{totp_code}",
|
||||
subtitle_markup=f"Type: remove {acc_name} • {
|
||||
subtitle_markup=f"Press Enter to remove • {
|
||||
time_display
|
||||
} remaining",
|
||||
icon_markup=icons.trash,
|
||||
action=lambda: None, # No action - shows instruction only
|
||||
action=lambda acc=acc_name: self._remove_account_and_refresh(acc),
|
||||
relevance=0.9,
|
||||
plugin_name=self.display_name,
|
||||
data={
|
||||
"type": "remove_instruction",
|
||||
"account": acc_name,
|
||||
"code": totp_code,
|
||||
"keep_launcher_open": True,
|
||||
"keep_launcher_open": True
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -655,9 +703,9 @@ class OTPPlugin(PluginBase):
|
||||
results.append(
|
||||
Result(
|
||||
title=f"Error: {acc_name}",
|
||||
subtitle=f"Type: remove {acc_name} (Invalid secret)",
|
||||
subtitle=f"Press Enter to remove (Invalid secret)",
|
||||
icon_markup=icons.trash,
|
||||
action=lambda: None, # No action - shows instruction only
|
||||
action=lambda acc=acc_name: self._remove_account_and_refresh(acc),
|
||||
relevance=0.8,
|
||||
plugin_name=self.display_name,
|
||||
data={
|
||||
@@ -689,114 +737,28 @@ class OTPPlugin(PluginBase):
|
||||
issuer = account_data.get("issuer", "")
|
||||
display_name = f"{issuer} - {account_name}" if issuer else account_name
|
||||
|
||||
# Remove the account
|
||||
try:
|
||||
del self.secrets[account_name]
|
||||
self._save_secrets()
|
||||
|
||||
return [
|
||||
Result(
|
||||
title=f"✓ Removed '{display_name}'",
|
||||
subtitle="OTP account removed successfully",
|
||||
icon_markup=icons.check,
|
||||
action=lambda: self._trigger_refresh(),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "success", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
except Exception as e:
|
||||
return [
|
||||
Result(
|
||||
title="Error removing account",
|
||||
subtitle=f"Failed to remove account: {str(e)}",
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
# Show confirmation for removal
|
||||
return [
|
||||
Result(
|
||||
title=f"Remove '{display_name}'?",
|
||||
subtitle="Press Enter to confirm removal",
|
||||
icon_markup=icons.trash,
|
||||
action=lambda acc=account_name: self._remove_account_and_refresh(acc),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "remove_confirm", "account": account_name,"keep_launcher_open": True,},
|
||||
)
|
||||
]
|
||||
|
||||
def _handle_base32_secret(self, account_name: str, secret: str) -> List[Result]:
|
||||
"""Handle raw Base32 secret."""
|
||||
try:
|
||||
# Clean up the secret - remove spaces, dashes, and convert to uppercase
|
||||
clean_secret = (
|
||||
secret.replace(" ", "").replace("-", "").replace("_", "").upper()
|
||||
)
|
||||
|
||||
# Remove any non-base32 characters
|
||||
import re
|
||||
|
||||
clean_secret = re.sub(r"[^A-Z2-7]", "", clean_secret)
|
||||
|
||||
# Add padding if needed (Base32 requires padding to multiple of 8)
|
||||
while len(clean_secret) % 8 != 0:
|
||||
clean_secret += "="
|
||||
|
||||
# Validate Base32 format
|
||||
try:
|
||||
base64.b32decode(clean_secret)
|
||||
except Exception as e:
|
||||
return [
|
||||
Result(
|
||||
title="Invalid Base32 secret",
|
||||
subtitle=f"Error: {str(e)}. Please check the secret format",
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
# Test if the secret can generate a valid TOTP
|
||||
try:
|
||||
test_totp = pyotp.TOTP(clean_secret)
|
||||
test_code = test_totp.now()
|
||||
if not test_code or len(test_code) != 6:
|
||||
raise ValueError("Generated invalid TOTP code")
|
||||
except Exception as e:
|
||||
return [
|
||||
Result(
|
||||
title="Invalid secret for TOTP",
|
||||
subtitle=f"Cannot generate TOTP: {str(e)}",
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
self.secrets[account_name] = {
|
||||
"secret": clean_secret,
|
||||
"issuer": "",
|
||||
"algorithm": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 30,
|
||||
}
|
||||
self._save_secrets()
|
||||
result = validate_base32_secret(secret)
|
||||
|
||||
if not result["success"]:
|
||||
return [
|
||||
Result(
|
||||
title=f"✓ Added '{account_name}'",
|
||||
subtitle=f"OTP account added successfully (secret: {
|
||||
clean_secret[:4]
|
||||
}...)",
|
||||
icon_markup=icons.check,
|
||||
action=lambda: self._trigger_refresh(),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "success", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
except Exception as e:
|
||||
return [
|
||||
Result(
|
||||
title="Error adding account",
|
||||
subtitle=f"Unexpected error: {str(e)}",
|
||||
title="Invalid Base32 secret",
|
||||
subtitle=result["error"],
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
@@ -805,79 +767,36 @@ class OTPPlugin(PluginBase):
|
||||
)
|
||||
]
|
||||
|
||||
self.secrets[account_name] = {
|
||||
"secret": result["secret"],
|
||||
"issuer": "",
|
||||
"algorithm": "SHA1",
|
||||
"digits": 6,
|
||||
"period": 30,
|
||||
}
|
||||
self._save_secrets()
|
||||
|
||||
return [
|
||||
Result(
|
||||
title=f"✓ Added '{account_name}'",
|
||||
subtitle=f"OTP account added successfully (secret: {result['secret'][:4]}...)",
|
||||
icon_markup=icons.check,
|
||||
action=lambda: self._trigger_refresh(),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "success", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
def _handle_otpauth_uri(self, account_name: str, uri: str) -> List[Result]:
|
||||
"""Handle otpauth:// URI."""
|
||||
try:
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
result = parse_otpauth_uri(uri, account_name)
|
||||
|
||||
parsed = urlparse(uri)
|
||||
if parsed.scheme != "otpauth" or parsed.netloc != "totp":
|
||||
return [
|
||||
Result(
|
||||
title="Invalid otpauth URI",
|
||||
subtitle="Only otpauth://totp/ URIs are supported",
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
if not account_name:
|
||||
account_path = parsed.path.lstrip("/")
|
||||
if ":" in account_path:
|
||||
issuer, extracted_name = account_path.split(":", 1)
|
||||
account_name = extracted_name
|
||||
else:
|
||||
account_name = account_path
|
||||
|
||||
params = parse_qs(parsed.query)
|
||||
secret = params.get("secret", [""])[0]
|
||||
issuer = params.get("issuer", [""])[0]
|
||||
algorithm = params.get("algorithm", ["SHA1"])[0]
|
||||
digits = int(params.get("digits", ["6"])[0])
|
||||
period = int(params.get("period", ["30"])[0])
|
||||
|
||||
if not secret:
|
||||
return [
|
||||
Result(
|
||||
title="No secret in URI",
|
||||
subtitle="otpauth URI must contain a secret parameter",
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
self.secrets[account_name] = {
|
||||
"secret": secret,
|
||||
"issuer": issuer,
|
||||
"algorithm": algorithm,
|
||||
"digits": digits,
|
||||
"period": period,
|
||||
}
|
||||
self._save_secrets()
|
||||
|
||||
display_name = f"{issuer} - {account_name}" if issuer else account_name
|
||||
return [
|
||||
Result(
|
||||
title=f"✓ Added '{display_name}'",
|
||||
subtitle="OTP account added from URI",
|
||||
icon_markup=icons.check,
|
||||
action=lambda: self._trigger_refresh(),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "success", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
except Exception as e:
|
||||
if not result["success"]:
|
||||
return [
|
||||
Result(
|
||||
title="Error parsing otpauth URI",
|
||||
subtitle=str(e),
|
||||
subtitle=result["error"],
|
||||
icon_markup=icons.cancel,
|
||||
action=lambda: None,
|
||||
relevance=0.5,
|
||||
@@ -885,3 +804,25 @@ class OTPPlugin(PluginBase):
|
||||
data={"type": "error", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
|
||||
self.secrets[result["account_name"]] = {
|
||||
"secret": result["secret"],
|
||||
"issuer": result["issuer"],
|
||||
"algorithm": result["algorithm"],
|
||||
"digits": result["digits"],
|
||||
"period": result["period"],
|
||||
}
|
||||
self._save_secrets()
|
||||
|
||||
display_name = f"{result['issuer']} - {result['account_name']}" if result['issuer'] else result['account_name']
|
||||
return [
|
||||
Result(
|
||||
title=f"✓ Added '{display_name}'",
|
||||
subtitle="OTP account added from URI",
|
||||
icon_markup=icons.check,
|
||||
action=lambda: self._trigger_refresh(),
|
||||
relevance=1.0,
|
||||
plugin_name=self.display_name,
|
||||
data={"type": "success", "keep_launcher_open": True},
|
||||
)
|
||||
]
|
||||
Reference in new issue
Block a user