noctalia
@@ -0,0 +1,16 @@
|
||||
# Backup files
|
||||
*.backup
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Runtime files
|
||||
pinned.json
|
||||
settings.json
|
||||
|
||||
# Build artifacts
|
||||
*.qmlc
|
||||
|
||||
# Development scripts
|
||||
i18n/all_languages.sh
|
||||
# User notecards - personal data
|
||||
notecards/*.json
|
||||
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButton {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
icon: "clipboard-data"
|
||||
tooltipText: pluginApi?.tr("bar.tooltip")
|
||||
tooltipDirection: BarService.getTooltipDirection(screen?.name)
|
||||
baseSize: Style.getCapsuleHeightForScreen(screen?.name)
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
|
||||
colorBg: Style.capsuleColor
|
||||
colorFg: Color.mOnSurface
|
||||
colorBgHover: Color.mHover
|
||||
colorFgHover: Color.mOnHover
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": pluginApi?.tr("context.toggle"),
|
||||
"action": "toggle-clipper",
|
||||
"icon": "clipboard"
|
||||
},
|
||||
{
|
||||
"label": pluginApi?.tr("context.settings"),
|
||||
"action": "open-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "toggle-clipper") {
|
||||
if (pluginApi?.togglePanel) {
|
||||
pluginApi.togglePanel(screen);
|
||||
}
|
||||
} else if (action === "open-settings") {
|
||||
if (pluginApi?.manifest) {
|
||||
BarService.openPluginSettings(screen, pluginApi.manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (pluginApi?.openPanel) {
|
||||
pluginApi.openPanel(screen, this);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
PanelService.showContextMenu(contextMenu, root, screen);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
# Clipper Plugin - Changelog
|
||||
|
||||
## Version 2.0.0 (2026-02-05)
|
||||
|
||||
### 🎯 Major Changes
|
||||
|
||||
**New Feature: Add Selection to NoteCard**
|
||||
- New IPC command: `addSelectionToNoteCard`
|
||||
- Captures selected text and presents context menu to add to existing notes or create new note
|
||||
- Text is automatically formatted as bullet points with "- " prefix
|
||||
- Integrated with Hyprland keybind: Super+V, X (chord)
|
||||
|
||||
**Translation System Overhaul**
|
||||
- Migrated from `I18n.tr()` to `pluginApi?.tr()` across all files
|
||||
- Added comprehensive `i18n/en.json` with 28 toast message keys
|
||||
- All user-facing strings now use translation system with fallbacks
|
||||
- Translation key structure: `component.key` (e.g., `toast.note-created`)
|
||||
|
||||
**Architecture Improvements**
|
||||
- Fixed IPC handlers to use `pluginApi.withCurrentScreen()` instead of direct `Quickshell.screens[0]` access
|
||||
- Added proper Component.onDestruction cleanup for 6 data structures (pinnedItems, noteCards, items, firstSeenById, imageCache, imageCacheOrder)
|
||||
- Memory leak prevention with proper process termination
|
||||
- Unified translation syntax: `pluginApi?.tr("key") || "fallback"`
|
||||
|
||||
### 📁 New Files
|
||||
|
||||
1. **NoteCardSelector.qml** (173 lines)
|
||||
- Fullscreen overlay for note selection menu
|
||||
- Synchronously loads notecards from files
|
||||
- Shows "Create New Note" + list of existing notes
|
||||
- Mouse-position-aware context menu
|
||||
- ESC to cancel, click outside to close
|
||||
|
||||
2. **NoteCard.qml** (~300 lines)
|
||||
- Updated visual style to match ClipboardCard
|
||||
- Icon + title left-aligned (not centered)
|
||||
- Transparent buttons without background
|
||||
- Increased font sizes (13-14px)
|
||||
- Auto-height expansion based on content
|
||||
- Drag handle on icon only (24px width)
|
||||
- All corners rounded on main card
|
||||
- Only top corners rounded on header
|
||||
|
||||
3. **NoteCardsPanel.qml** (updated)
|
||||
- Background click-to-close functionality
|
||||
- Controls moved to top-left corner
|
||||
- Note counter and add button repositioned
|
||||
- Starting Y position increased to 80px to avoid control overlap
|
||||
|
||||
### 🔧 Files Modified
|
||||
|
||||
**Main.qml** (+250 lines, massive refactoring)
|
||||
|
||||
*New IPC Handlers:*
|
||||
- `addSelectionToNoteCard()` - Main entry point for feature
|
||||
- Uses same pattern as `addSelectionToTodo()` for consistency
|
||||
|
||||
*New Functions:*
|
||||
- `getSelectionAndShowNoteSelector()` - Triggers selection capture
|
||||
- `showNoteCardSelector(text)` - Displays note selection menu
|
||||
- `handleNoteCardSelected(noteId, noteTitle)` - Handles existing note selection
|
||||
- `handleCreateNewNoteFromSelection()` - Creates new note with bullet text
|
||||
- `appendTextToNoteCard(noteId, text)` - Adds bullet point to existing note
|
||||
|
||||
*New Process:*
|
||||
- `getSelectionForNoteSelectorProcess` - Captures primary selection via wl-paste
|
||||
- StdioCollector: `noteSelectionStdout`
|
||||
- Calls `showNoteCardSelector()` on successful capture
|
||||
|
||||
*New Variants:*
|
||||
- `NoteCardSelector` instances for each screen
|
||||
- Registered as `noteCardSelector` property
|
||||
- Connected to signal handlers for note selection/creation
|
||||
|
||||
*New Properties:*
|
||||
- `noteCardSelector: null` - Reference to selector instance
|
||||
- `pendingNoteCardText: ""` - Stores captured selection
|
||||
|
||||
*Translation Updates:*
|
||||
- All 28 Toast messages now use `pluginApi?.tr()`
|
||||
- String interpolation for dynamic values ({max}, {fileName}, {count})
|
||||
- Consistent error handling with translated messages
|
||||
|
||||
*Bug Fixes:*
|
||||
- Fixed `createNoteCard()` to call `saveNoteCard()` after creation
|
||||
- Removed duplicate `saveNoteCard(newNote)` calls from wrong functions
|
||||
- Fixed Component.onDestruction syntax error (duplicate `}`)
|
||||
|
||||
**i18n/en.json** (+30 keys)
|
||||
|
||||
*New Toast Section:*
|
||||
```json
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"note-created": "Note created",
|
||||
"note-not-found": "Note not found",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
// ... 22 more keys
|
||||
}
|
||||
```
|
||||
|
||||
**NoteCard.qml** (visual redesign)
|
||||
- Header layout changed from centered to left-aligned
|
||||
- Icon (note) + title in RowLayout
|
||||
- Drag handle isolated to icon Item (24px width)
|
||||
- Separate MouseArea for title focus
|
||||
- Transparent button styling: `colorBg: "transparent"`
|
||||
- Font changes: removed bold, increased size to 14px
|
||||
- Radius system:
|
||||
- Main card: `radius: Style.radiusM` (all corners)
|
||||
- Header: `topLeftRadius/topRightRadius: Style.radiusM`, bottom: 0
|
||||
- Auto-height expansion via ScrollView content measurement
|
||||
- Size changed: 300x180 → 350x280px
|
||||
- Title input wider, better usability
|
||||
|
||||
**NoteCardsPanel.qml**
|
||||
- Background MouseArea with `z: -1` for click-to-close
|
||||
- Controls repositioned to `anchors.top` + `anchors.left`
|
||||
- Note counter + add button in top-left corner
|
||||
- 16px margins from edges
|
||||
|
||||
**ClipboardCard.qml**
|
||||
- Applied same radius pattern as NoteCard
|
||||
- Main card: all corners rounded
|
||||
- Header: top corners only
|
||||
|
||||
**BarWidget.qml**
|
||||
- Migrated `I18n.tr()` → `pluginApi?.tr()`
|
||||
- Keys: `bar.tooltip`, `context.toggle`, `context.settings`
|
||||
|
||||
**Settings.qml**
|
||||
- Already had full translation coverage
|
||||
- Verified all labels use `pluginApi?.tr()`
|
||||
|
||||
### 🎨 UI/UX Changes
|
||||
|
||||
**NoteCard Visual Updates:**
|
||||
- More modern, cleaner look matching ClipboardCard
|
||||
- Better readability with larger fonts
|
||||
- Improved drag interaction (icon-only handle)
|
||||
- Responsive to global radius settings (Style.radiusM)
|
||||
- Auto-expands height based on content
|
||||
|
||||
**NoteCardsPanel Improvements:**
|
||||
- Better spatial organization (controls top-left)
|
||||
- No accidental close when clicking on notes
|
||||
- Clear visual hierarchy
|
||||
|
||||
**Context Menu for Note Selection:**
|
||||
- Appears at cursor position
|
||||
- Smooth 150ms delay for compositor
|
||||
- First option always "Create New Note"
|
||||
- Separator before existing notes
|
||||
- Note icons for visual distinction
|
||||
|
||||
### ⚙️ Technical Improvements
|
||||
|
||||
**Memory Management:**
|
||||
- Cleanup of 6 data structures in Component.onDestruction
|
||||
- Process termination for `getSelectionForNoteSelectorProcess`
|
||||
- Proper array clearing: `items = []`, `imageCache = {}`
|
||||
|
||||
**Code Quality:**
|
||||
- Consistent translation syntax across codebase
|
||||
- No hardcoded user-facing strings
|
||||
- Proper null-safe operators: `pluginApi?.tr()`
|
||||
- Immutable array updates maintained
|
||||
|
||||
**IPC Architecture:**
|
||||
- Correct use of `pluginApi.withCurrentScreen()`
|
||||
- No direct `Quickshell.screens` access in IPC handlers
|
||||
- Consistent pattern with existing `addSelectionToTodo`
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
1. **Syntax Errors:**
|
||||
- Fixed duplicate `}` in Component.onDestruction
|
||||
- Removed extra closing braces in function declarations
|
||||
- Fixed missing `} catch(e) {` block in loadNoteCardsProc
|
||||
|
||||
2. **Runtime Errors:**
|
||||
- Fixed `ReferenceError: newNote is not defined` by removing invalid `saveNoteCard(newNote)` calls from functions that don't have access to `newNote`
|
||||
- Only `createNoteCard()` calls `saveNoteCard(newNote)` now
|
||||
|
||||
3. **Functional Issues:**
|
||||
- Fixed note selector not showing existing notes (timing issue with async loading)
|
||||
- Fixed "Create New Note" not working (signal connection verified)
|
||||
- Fixed notes not being saved to files (added `saveNoteCard()` call)
|
||||
|
||||
### 📝 Usage Examples
|
||||
|
||||
**IPC Commands:**
|
||||
```bash
|
||||
# Add selected text to note via keybind
|
||||
qs -c noctalia-shell ipc call plugin:clipper addSelectionToNoteCard
|
||||
|
||||
# Or with dev repo
|
||||
qs -c ~/dev/repo/noctalia ipc call plugin:clipper addSelectionToNoteCard
|
||||
```
|
||||
|
||||
**Hyprland Keybind (chord):**
|
||||
```bash
|
||||
# In ~/.config/hypr/keybind.conf
|
||||
binds = SUPER_L, V&C, exec, qs -c ~/dev/repo/noctalia ipc call plugin:clipper addSelectionToTodo
|
||||
binds = SUPER_L, V&X, exec, qs -c ~/dev/repo/noctalia ipc call plugin:clipper addSelectionToNoteCard
|
||||
```
|
||||
|
||||
**User Flow:**
|
||||
1. Select text anywhere
|
||||
2. Press Super+V (opens chord mode)
|
||||
3. Press X (triggers addSelectionToNoteCard)
|
||||
4. Context menu appears at cursor
|
||||
5. Click "Create New Note" or select existing note
|
||||
6. Text is added as bullet point: `- selected text`
|
||||
|
||||
### 📊 Statistics
|
||||
|
||||
**Lines Changed:**
|
||||
- Main.qml: +250 lines
|
||||
- NoteCard.qml: ~300 lines (redesigned)
|
||||
- NoteCardSelector.qml: +173 lines (new)
|
||||
- NoteCardsPanel.qml: +50 lines
|
||||
- i18n/en.json: +30 keys
|
||||
- Total: ~800 lines added/modified
|
||||
|
||||
**Files:**
|
||||
- New: 1 file (NoteCardSelector.qml)
|
||||
- Modified: 7 files
|
||||
- Total: 8 files changed
|
||||
|
||||
**Translation Coverage:**
|
||||
- 28 new toast message keys
|
||||
- 100% coverage for user-facing strings
|
||||
- All components use pluginApi?.tr()
|
||||
|
||||
### 🔄 Breaking Changes
|
||||
|
||||
**Translation System:**
|
||||
- Old: `I18n.tr("clipboard.key", "Fallback")`
|
||||
- New: `pluginApi?.tr("component.key") || "Fallback"`
|
||||
- All plugins must update if they were using I18n
|
||||
|
||||
**IPC Handlers:**
|
||||
- Now use `pluginApi.withCurrentScreen()` instead of `Quickshell.screens[0]`
|
||||
- More robust multi-screen support
|
||||
|
||||
### 🚀 Next Steps (Future Enhancements)
|
||||
|
||||
1. **i18n Completion:**
|
||||
- Translate en.json to all other language files (de, es, fr, etc.)
|
||||
- Maintain identical structure across all languages
|
||||
|
||||
2. **Feature Enhancements:**
|
||||
- Resizable note cards
|
||||
- Rich text formatting
|
||||
- Note categories/tags
|
||||
- Search functionality
|
||||
|
||||
3. **Performance:**
|
||||
- Lazy loading for large note collections
|
||||
- Virtual scrolling if >20 notes
|
||||
|
||||
4. **Integration:**
|
||||
- Share notes between devices
|
||||
- Sync with cloud services
|
||||
|
||||
---
|
||||
|
||||
**Version:** 2.0.0
|
||||
**Date:** 2026-02-05
|
||||
**Author:** Development Team
|
||||
**Reviewed by:** Code Review (QML-code-reviewer.md)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Changelog v2.1.0
|
||||
|
||||
## Release Date
|
||||
2026-02-08
|
||||
|
||||
## New Features
|
||||
|
||||
### Show Close Button
|
||||
- Added optional X button in top-right corner of panel header
|
||||
- Toggle available in Settings > General
|
||||
- Provides alternative way to close the panel
|
||||
- Button styled with error color on hover for clear visual feedback
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
### Clipboard Selection Fix
|
||||
- Fixed critical bug where only the last copied item could be selected from history
|
||||
- Fixed issue where clipboard items couldn't be copied back to system clipboard
|
||||
- Changed from two-process approach to direct shell pipe: `cliphist decode ID | wl-copy`
|
||||
- Removed non-existent `stdout.data` API usage
|
||||
|
||||
### Note Persistence Fix
|
||||
- Fixed bug where notes didn't persist after reopening panel
|
||||
- Added missing `saveNoteCard()` call in `updateNoteCard()` function
|
||||
- Notes now properly save to disk when modified
|
||||
|
||||
## Improvements
|
||||
|
||||
### Settings UI Enhancement
|
||||
- Reorganized settings into tabbed interface using NTabs
|
||||
- **General Tab**: Integrations, Features (PinCards/NoteCards toggles), Show Close Button
|
||||
- **Appearance Tab**: Card color customization
|
||||
- Improved settings organization and discoverability
|
||||
- Added feature toggles with item counters showing current usage
|
||||
|
||||
### Translation System Synchronization
|
||||
- Synchronized all 16 language files with consistent structure
|
||||
- Removed unused translation keys
|
||||
- All translation files now have identical key structure
|
||||
- Maintained existing translations while adding new keys
|
||||
|
||||
### Code Quality
|
||||
- Removed experimental "Close on Background Click" feature (not implementable in current Noctalia architecture)
|
||||
- Cleaned up debug logging
|
||||
- Removed temporary backup files
|
||||
- Improved code organization
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Fixed Functions
|
||||
- `copyToClipboard()` in Main.qml (line 742-755)
|
||||
- `updateNoteCard()` in Main.qml (added saveNoteCard call)
|
||||
|
||||
### Modified Components
|
||||
- Settings.qml: Restructured with NTabBar/NTabView
|
||||
- Panel.qml: Added optional close button with reactive visibility
|
||||
- i18n/*.json: All 16 language files synchronized
|
||||
|
||||
### Removed Features
|
||||
- "Close on Background Click" toggle and related code
|
||||
- Panel behavior section in settings
|
||||
- Related translation keys and properties
|
||||
|
||||
## Breaking Changes
|
||||
None - all changes are backward compatible
|
||||
|
||||
## Known Issues
|
||||
None
|
||||
|
||||
## Upgrade Notes
|
||||
- Plugin will automatically migrate settings
|
||||
- No user action required
|
||||
- Existing pinned items and notecards will be preserved
|
||||
@@ -0,0 +1,70 @@
|
||||
# Changelog v2.2.3
|
||||
|
||||
## Release Date
|
||||
**2026-03-16**
|
||||
|
||||
## New Features
|
||||
|
||||
#### Control Center Shortcut
|
||||
- Added Control Center Widget shortcut to Control Center
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
#### Clear All Notes Fix
|
||||
- Fixed the bug where you click Clear All Notes and reopen the plugin panel but the notes are still there
|
||||
|
||||
#### Panel Corners Fix
|
||||
- Fixed the plugin's panel bottom corners has square corners when the top is rounded
|
||||
|
||||
#### Export Notes Fix
|
||||
- Fixed you can export a Note many times but when deleting you can only delete one so the other notes still stucking inside your `~/Documents/` directory. Now you can still export a note many times, changing the contents and all the notes were exported from that one note will all be deleted
|
||||
|
||||
#### Applying Settings
|
||||
- Settings will now only apply when you click the `Apply` button instead of immediately applying the settings (this is to make use of the apply button)
|
||||
|
||||
#### Panel Close Button
|
||||
- Fixed an issue where clicking the NoteCards panel would close the plugin panel even when the Close Button was enabled. The panel now only closes on background click when the Close Button is disabled. When enabled, use the Close Button or click outside the plugin's panel to close the panel.
|
||||
|
||||
#### Shell Injection via File Paths
|
||||
- Fixed potential RCE in `deleteNoteCard()` where `exportedFiles` entries from stored JSON were passed directly to `rm` without validation, each filename is now checked against a strict pattern before deletion
|
||||
- Fixed `clearAllNoteCards()` where a dangling `safePattern` validation referenced undefined variables and an unsafe `sh -c` glob command still ran beneath it, replaced with a loop over each note's tracked `exportedFiles`, validated per entry, with no shell involved
|
||||
- Escaped the dot in the safe pattern regex from `.txt` to `\.txt` to match a literal dot instead of any character
|
||||
|
||||
#### Empty Catch Blocks
|
||||
- Fixed empty `catch (e) {}` blocks in `Settings.qml` `onCompleted` that silently swallowed JSON parse failures, both `cardColors` and `customColors` catch blocks now log via `Logger.w`
|
||||
|
||||
#### Duplicate Settings Loading
|
||||
- Fixed `cardColors` being loaded twice in `Settings.qml` `onCompleted`, removed the first incomplete block that lacked `pendingCardColors` assignment and kept the second complete one
|
||||
|
||||
## Improvements
|
||||
|
||||
#### UI Enhancement
|
||||
- Panel will now attach to bar instead of being separate
|
||||
- Decrease abit of panel's width and height for a cleaner look
|
||||
- Added active ring, badge counts for filter buttons
|
||||
|
||||
#### Keybind Changes
|
||||
- Pressing `Alt+1` will now navigate you back to `All` instead of `Alt+0`, `Alt+2` is `Text` and goes on
|
||||
|
||||
#### Clipboard Mouse Wheel Scroll
|
||||
- You can now use the mouse wheel scroll to navigate left and right between clipboards
|
||||
|
||||
#### Note Card and Pinned Item Separator
|
||||
- Added a separator between Note Card and Pinned Item
|
||||
|
||||
#### Panel Close Button
|
||||
- Moved the Close Button from next to Open Settings to the panel's top right corner
|
||||
|
||||
## Breaking Changes
|
||||
- Might be - please report if there are bugs
|
||||
|
||||
## Known Issues
|
||||
- After adding a clipboard into ToDo plugin you can't check the complete box or remove that clipboard from ToDo
|
||||
|
||||
## Upgrade Notes
|
||||
- Plugin will automatically migrate settings
|
||||
- No user action required
|
||||
- Existing pinned items and notecards will be preserved
|
||||
|
||||
## To Do
|
||||
- Add `Annotation Tool` support for Images clipboards
|
||||
@@ -0,0 +1,76 @@
|
||||
# Changelog v2.3.0
|
||||
|
||||
## Release Date
|
||||
**2026-03-19**
|
||||
|
||||
## New Features
|
||||
|
||||
#### Fullscreen Mode
|
||||
- New toggle in Settings → Features: **Fullscreen Mode**
|
||||
- Expands the panel to fill the entire screen vertically and horizontally
|
||||
- Clipboard panel (cliphist) stays anchored to the bottom
|
||||
- PinCards and NoteCards remain fully accessible in fullscreen
|
||||
- Panel width expands to full screen width (`screen.width`)
|
||||
- Panel height expands to full screen height (`screen.height`)
|
||||
|
||||
#### Hide Panel Background
|
||||
- New toggle in Settings → Features: **Hide Panel Background**
|
||||
- Makes the SmartPanel background transparent — cards float over the desktop blur or wallpaper
|
||||
- Clipboard panel (cliphist) and PinCards retain their own background for readability
|
||||
- Clicking anywhere on the transparent background closes the panel (backdrop dismiss)
|
||||
- Works best combined with Noctalia's background blur effect
|
||||
|
||||
#### Auto-Paste on Click
|
||||
- New toggle in Settings → Auto-Paste: **Auto-Paste on Click**
|
||||
- After selecting a clipboard item, the panel closes and the content is automatically pasted into the focused window
|
||||
- Uses `wtype -M ctrl -M shift v` (Ctrl+Shift+V) — works in terminals and most applications
|
||||
- **Requires `wtype`** — install with `sudo pacman -S wtype` (available in cachyos-extra-v3 / Arch AUR)
|
||||
- Warning box appears in settings when `wtype` is not installed
|
||||
|
||||
#### Auto-Paste Right-Click Mode
|
||||
- Sub-toggle: **Right-Click Only** (visible when Auto-Paste is enabled)
|
||||
- When enabled: left-click copies to clipboard normally; right-click copies + pastes
|
||||
- When disabled: left-click triggers auto-paste; right-click does nothing special
|
||||
- Works on both clipboard list items and pinned items
|
||||
|
||||
#### Auto-Paste Delay Slider
|
||||
- Slider: **Paste Delay (ms)** — range 100–1000 ms, step 50, default 300 ms
|
||||
- Allows tuning the delay between panel close and paste trigger
|
||||
- Increase if your compositor uses focus-follows-cursor (focus needs time to return to target window)
|
||||
|
||||
## Improvements
|
||||
|
||||
#### NSlider component fix
|
||||
- Replaced standard Qt `Slider` component with Noctalia's `NValueSlider` widget
|
||||
- Consistent look and feel with the rest of the settings UI
|
||||
|
||||
## i18n
|
||||
|
||||
#### 12 new translation keys added (all 17 languages)
|
||||
- `settings.fullscreen-mode` / `settings.fullscreen-mode-desc`
|
||||
- `settings.hide-panel-background` / `settings.hide-panel-background-desc`
|
||||
- `settings.auto-paste-section`
|
||||
- `settings.auto-paste` / `settings.auto-paste-desc`
|
||||
- `settings.auto-paste-warning`
|
||||
- `settings.auto-paste-rmb` / `settings.auto-paste-rmb-desc`
|
||||
- `settings.auto-paste-delay` / `settings.auto-paste-delay-desc`
|
||||
|
||||
Full translations provided for: de, es, fr, pl. English fallback for all other languages.
|
||||
Italian (`it.json`) — new language file added.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- `Panel.qml`: Added `panelBackgroundColor` property — allows plugin to override SmartPanel background color reactively
|
||||
- `PluginPanelSlot.qml` (Noctalia core): Added forwarding of `panelBackgroundColor` from plugin instance to SmartPanel
|
||||
- `Main.qml`: Added `wtypeAvailable` detection via `which wtype` on startup; `autoPasteTimer` + `autoPasteProc` with full cleanup in `Component.onDestruction`
|
||||
- `ClipboardCard.qml`: Added `signal rightClicked`; both MouseAreas updated to handle `Qt.RightButton`
|
||||
|
||||
## Known Issues
|
||||
- After adding a clipboard into ToDo plugin you can't check the complete box or remove that clipboard from ToDo
|
||||
- Auto-Paste uses Ctrl+Shift+V which works in terminals; for GUI-only apps Ctrl+V would be sufficient — a "Terminal Mode" toggle may be added in a future release
|
||||
|
||||
## Upgrade Notes
|
||||
- Plugin will automatically migrate settings — three new fields added to `settings.json`: `fullscreenMode`, `hidePanelBackground`, `autoPaste`, `autoPasteOnRightClick`, `autoPasteDelay`
|
||||
- No user action required
|
||||
- Existing pinned items and notecards are preserved
|
||||
- `wtype` must be installed separately for Auto-Paste to function
|
||||
@@ -0,0 +1,400 @@
|
||||
# Clipper Plugin - Code Review Report v2.0
|
||||
|
||||
**Date:** 2026-02-05
|
||||
**Reviewer:** QML Code Reviewer (following QML-code-reviewer.md)
|
||||
**Plugin:** Clipper v2.0.0
|
||||
**Status:** ✅ **APPROVED** - No CRITICAL or HIGH issues found
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The Clipper plugin has been thoroughly reviewed against Noctalia development standards. The codebase demonstrates **excellent architecture**, proper **IPC patterns**, comprehensive **translation system**, and appropriate **memory management**.
|
||||
|
||||
**Overall Grade:** ⭐⭐⭐⭐⭐ (5/5)
|
||||
|
||||
---
|
||||
|
||||
## Review Criteria Results
|
||||
|
||||
### ✅ Architecture (PASS)
|
||||
- [x] No internal IPC calls - all IPC is external-facing only
|
||||
- [x] Required `toggle()` function present in IpcHandler
|
||||
- [x] Proper use of `pluginApi.withCurrentScreen()` (no direct Quickshell.screens access)
|
||||
- [x] Clean separation of concerns (BarWidget, Panel, Settings, Main)
|
||||
- [x] IpcHandler target matches plugin name: `plugin:clipper`
|
||||
|
||||
### ✅ Memory Management (PASS)
|
||||
- [x] Component.onDestruction in Main.qml with comprehensive cleanup
|
||||
- [x] 13 Process objects properly terminated
|
||||
- [x] 6 data structures cleared (pinnedItems, noteCards, items, firstSeenById, imageCache, imageCacheOrder)
|
||||
- [x] NoteCardSelector has proper cleanup (Timer stopped)
|
||||
- [x] No memory leak patterns detected
|
||||
|
||||
### ✅ Translation System (PASS)
|
||||
- [x] All user-facing strings use `pluginApi?.tr()` with fallbacks
|
||||
- [x] i18n/en.json exists with proper structure (no plugin name prefix)
|
||||
- [x] All keys use kebab-case format (no snake_case)
|
||||
- [x] 28 toast message keys properly organized
|
||||
- [x] Consistent translation syntax across all files
|
||||
- [x] String interpolation uses `{key}` syntax
|
||||
|
||||
### ✅ IPC Interface (PASS)
|
||||
- [x] 11 IPC functions properly implemented
|
||||
- [x] All functions use `pluginApi.withCurrentScreen()` pattern
|
||||
- [x] Clear documentation comments for each IPC command
|
||||
- [x] External-facing only (no internal Process calls to own IPC)
|
||||
|
||||
### ✅ Code Quality (PASS)
|
||||
- [x] Consistent naming conventions
|
||||
- [x] Proper error handling with translated messages
|
||||
- [x] No console.log in production code
|
||||
- [x] Settings properly use `pluginApi.pluginSettings`
|
||||
- [x] immutable array updates throughout
|
||||
|
||||
---
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### 🟢 STRENGTHS
|
||||
|
||||
#### 1. Exceptional IPC Architecture
|
||||
**Main.qml:870-945**
|
||||
```qml
|
||||
IpcHandler {
|
||||
target: "plugin:clipper"
|
||||
|
||||
function openPanel() {
|
||||
if (root.pluginApi) {
|
||||
root.pluginApi.withCurrentScreen(screen => {
|
||||
root.pluginApi.openPanel(screen);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
togglePanel();
|
||||
}
|
||||
|
||||
// ... 9 more well-documented functions
|
||||
}
|
||||
```
|
||||
**Why this is good:**
|
||||
- Correct use of `pluginApi.withCurrentScreen()` instead of `Quickshell.screens[0]`
|
||||
- Required `toggle()` function present for keybind compatibility
|
||||
- All functions external-facing with usage documentation
|
||||
- Follows Noctalia best practices perfectly
|
||||
|
||||
#### 2. Comprehensive Memory Management
|
||||
**Main.qml:1143-1159**
|
||||
```qml
|
||||
Component.onDestruction: {
|
||||
// Terminate 13 processes
|
||||
if (listProc.running) listProc.terminate();
|
||||
if (decodeProc.running) decodeProc.terminate();
|
||||
// ... 11 more processes
|
||||
|
||||
// Clear 6 data structures
|
||||
pinnedItems = [];
|
||||
noteCards = [];
|
||||
items = [];
|
||||
firstSeenById = {};
|
||||
imageCache = {};
|
||||
imageCacheOrder = [];
|
||||
}
|
||||
```
|
||||
**Why this is good:**
|
||||
- Prevents memory leaks by terminating all background processes
|
||||
- Clears large data structures
|
||||
- Follows memory leak prevention best practices
|
||||
- Will not grow memory usage over time
|
||||
|
||||
#### 3. Perfect Translation System
|
||||
**i18n/en.json Structure:**
|
||||
```json
|
||||
{
|
||||
"bar": { "tooltip": "..." },
|
||||
"panel": { ... },
|
||||
"toast": { ... },
|
||||
"settings": { ... }
|
||||
}
|
||||
```
|
||||
**Why this is good:**
|
||||
- No plugin name prefix (correct pattern)
|
||||
- Component-based organization
|
||||
- All keys use kebab-case
|
||||
- 28 toast messages properly translated
|
||||
- String interpolation with `{key}` syntax
|
||||
|
||||
**Usage:**
|
||||
```qml
|
||||
// BarWidget.qml:16
|
||||
tooltipText: pluginApi?.tr("bar.tooltip") || "Clipboard History"
|
||||
|
||||
// Main.qml:239
|
||||
ToastService.showWarning((pluginApi?.tr("toast.max-pinned-items") || "Maximum {max} pinned items reached").replace("{max}", maxPinnedItems));
|
||||
```
|
||||
|
||||
#### 4. New Feature: addSelectionToNoteCard
|
||||
**Implementation Quality: Excellent**
|
||||
- Follows same pattern as `addSelectionToTodo` (consistency)
|
||||
- Proper Process for selection capture (`wl-paste`)
|
||||
- NoteCardSelector component properly isolated
|
||||
- Signal-based communication between components
|
||||
- Bullet point formatting implemented correctly
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Recommendations
|
||||
|
||||
### [MEDIUM] Missing Component.onDestruction in Minor Components
|
||||
**Files:** NoteCard.qml, NoteCardsPanel.qml, ClipboardCard.qml, Panel.qml
|
||||
|
||||
**Current Status:** These components don't have timers or processes, so no memory leaks.
|
||||
|
||||
**Recommendation:** Add empty Component.onDestruction for future-proofing:
|
||||
```qml
|
||||
Component.onDestruction: {
|
||||
// Cleanup will go here if timers/processes are added
|
||||
}
|
||||
```
|
||||
|
||||
**Priority:** Medium (not urgent, but good practice)
|
||||
|
||||
---
|
||||
|
||||
### [MEDIUM] Debug Logging Still Present
|
||||
**Files:** Main.qml, NoteCardSelector.qml
|
||||
|
||||
**Found:**
|
||||
```qml
|
||||
Logger.i("clipper", "handleCreateNewNoteFromSelection called, pendingText: " + root.pendingNoteCardText);
|
||||
Logger.i("NoteCardSelector", "onTriggered called, action: " + action);
|
||||
Logger.i("NoteCardSelector", "Emitting createNewNote signal");
|
||||
```
|
||||
|
||||
**Recommendation:** Remove debug logging before production release:
|
||||
```qml
|
||||
// Remove these lines:
|
||||
// Logger.i("clipper", "handleCreateNewNoteFromSelection called...");
|
||||
// Logger.i("NoteCardSelector", "onTriggered called...");
|
||||
```
|
||||
|
||||
**Priority:** Medium (helps performance, reduces log noise)
|
||||
|
||||
---
|
||||
|
||||
## Low Priority Suggestions
|
||||
|
||||
### [LOW] Translation File Synchronization
|
||||
**Current Status:** Only `i18n/en.json` has full translation coverage (28 toast keys)
|
||||
|
||||
**Recommendation:** Update other language files to match en.json structure:
|
||||
```bash
|
||||
cd ~/.config/noctalia/plugins/clipper/i18n
|
||||
# Copy en.json structure to all language files
|
||||
for lang in de es fr it pt nl ru ja zh-CN; do
|
||||
# Manually translate or use i18n service
|
||||
# Ensure same structure: jq 'keys' en.json == jq 'keys' $lang.json
|
||||
done
|
||||
```
|
||||
|
||||
**Priority:** Low (English fallbacks work, but full i18n is better UX)
|
||||
|
||||
---
|
||||
|
||||
### [LOW] Magic Numbers in NoteCard
|
||||
**File:** NoteCard.qml
|
||||
|
||||
**Found:**
|
||||
```qml
|
||||
width: 350
|
||||
height: 280
|
||||
Layout.preferredWidth: 24 // drag handle
|
||||
font.pixelSize: 14
|
||||
font.pixelSize: 13
|
||||
```
|
||||
|
||||
**Recommendation:** Use Style constants:
|
||||
```qml
|
||||
width: 350 // OK - card size, not a style constant
|
||||
height: 280 // OK
|
||||
Layout.preferredWidth: Style.iconSizeM // if exists
|
||||
font.pixelSize: Style.fontSizeM
|
||||
```
|
||||
|
||||
**Priority:** Low (current values are reasonable, not critical)
|
||||
|
||||
---
|
||||
|
||||
## File-by-File Analysis
|
||||
|
||||
### Main.qml ✅ EXCELLENT
|
||||
- **Lines:** ~1170
|
||||
- **IPC Functions:** 11 (all properly implemented)
|
||||
- **Memory Management:** Perfect (13 processes + 6 data structures)
|
||||
- **Translations:** 28 toast keys, all using pluginApi?.tr()
|
||||
- **Architecture:** Clean, well-organized, follows all best practices
|
||||
|
||||
**Highlights:**
|
||||
- `addSelectionToNoteCard` implementation excellent
|
||||
- Process management exemplary
|
||||
- No internal IPC calls
|
||||
- Proper use of pluginApi throughout
|
||||
|
||||
---
|
||||
|
||||
### NoteCardSelector.qml ✅ GOOD
|
||||
- **Lines:** ~173
|
||||
- **Purpose:** Fullscreen overlay for note selection
|
||||
- **Memory Management:** Timer cleanup present
|
||||
- **Translations:** Uses pluginApi?.tr() correctly
|
||||
|
||||
**Minor Issue:**
|
||||
- Debug logging present (Logger.i calls) - remove before prod
|
||||
|
||||
---
|
||||
|
||||
### NoteCard.qml ✅ GOOD
|
||||
- **Lines:** ~300
|
||||
- **Visual Design:** Modern, matches ClipboardCard
|
||||
- **Translations:** All strings use pluginApi?.tr()
|
||||
- **No timers/processes:** No cleanup needed
|
||||
|
||||
**Highlights:**
|
||||
- Drag handle isolated to icon (good UX)
|
||||
- Auto-height expansion
|
||||
- Responsive to Style.radiusM
|
||||
|
||||
---
|
||||
|
||||
### BarWidget.qml ✅ PERFECT
|
||||
- **Lines:** ~60
|
||||
- **Translations:** 3/3 strings use pluginApi?.tr()
|
||||
- **Context Menu:** Properly implemented
|
||||
- **No issues found**
|
||||
|
||||
---
|
||||
|
||||
### i18n/en.json ✅ PERFECT
|
||||
- **Structure:** Component-based (no plugin prefix) ✅
|
||||
- **Naming:** All kebab-case ✅
|
||||
- **Coverage:** 28 toast keys + bar/panel/settings ✅
|
||||
- **Interpolation:** Uses `{key}` syntax ✅
|
||||
|
||||
**Example of Excellence:**
|
||||
```json
|
||||
{
|
||||
"toast": {
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"text-added-to-note": "Text added to note"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
### Functional Tests
|
||||
- [ ] Test `addSelectionToNoteCard` with various text selections
|
||||
- [ ] Verify "Create New Note" creates note with bullet point
|
||||
- [ ] Verify selecting existing note appends text as bullet
|
||||
- [ ] Test with 0 notes, 1 note, and multiple notes
|
||||
- [ ] Verify note selector appears at cursor position
|
||||
|
||||
### Memory Tests
|
||||
```bash
|
||||
# Monitor memory usage over time
|
||||
watch -n 1 'ps -o rss,vsz -p $(pgrep quickshell)'
|
||||
|
||||
# Create/delete many notes to test cleanup
|
||||
for i in {1..50}; do
|
||||
qs -c noctalia-shell ipc call plugin:clipper addNoteCard "Test $i"
|
||||
# Delete note
|
||||
done
|
||||
# Memory should return to baseline
|
||||
```
|
||||
|
||||
### Translation Tests
|
||||
```bash
|
||||
# Verify all language files have same structure
|
||||
cd ~/.config/noctalia/plugins/clipper/i18n
|
||||
for f in *.json; do
|
||||
echo "$f: $(jq 'keys' $f | wc -l) top-level keys"
|
||||
done
|
||||
# All should show same count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compliance Matrix
|
||||
|
||||
| Criterion | Status | Evidence |
|
||||
|-----------|--------|----------|
|
||||
| No internal IPC calls | ✅ PASS | No Process calls to plugin:clipper |
|
||||
| Required toggle() function | ✅ PASS | Main.qml:898-900 |
|
||||
| pluginApi.withCurrentScreen() | ✅ PASS | All IPC handlers use it |
|
||||
| Component.onDestruction | ✅ PASS | Main.qml:1143, NoteCardSelector |
|
||||
| Translation system | ✅ PASS | pluginApi?.tr() everywhere |
|
||||
| i18n structure | ✅ PASS | No plugin prefix, kebab-case |
|
||||
| Memory leak prevention | ✅ PASS | 13 processes + 6 data structures |
|
||||
| Settings persistence | ✅ PASS | Uses pluginApi.saveSettings() |
|
||||
| Code quality | ✅ PASS | Clean, maintainable, documented |
|
||||
|
||||
---
|
||||
|
||||
## Final Verdict
|
||||
|
||||
### ✅ APPROVED FOR PRODUCTION
|
||||
|
||||
**Summary:**
|
||||
The Clipper plugin v2.0.0 demonstrates **exceptional quality** across all review criteria. The code follows Noctalia best practices, implements proper memory management, uses the translation system correctly, and has a well-designed architecture.
|
||||
|
||||
**Strengths:**
|
||||
- Perfect IPC implementation (11 functions, all external-facing)
|
||||
- Comprehensive memory cleanup (13 processes + 6 data structures)
|
||||
- Excellent translation coverage (28 toast keys, kebab-case, no prefix)
|
||||
- New feature `addSelectionToNoteCard` well-integrated
|
||||
- No CRITICAL or HIGH severity issues
|
||||
|
||||
**Recommendations for Future Releases:**
|
||||
1. Remove debug Logger.i calls (MEDIUM)
|
||||
2. Sync i18n files across all languages (LOW)
|
||||
3. Add empty Component.onDestruction to minor components (LOW)
|
||||
|
||||
**Grade:** ⭐⭐⭐⭐⭐ (5/5)
|
||||
|
||||
---
|
||||
|
||||
## Appendix: IPC Command Reference
|
||||
|
||||
All 11 IPC commands in Clipper plugin:
|
||||
|
||||
```bash
|
||||
# Panel Management
|
||||
qs -c noctalia-shell ipc call plugin:clipper toggle
|
||||
qs -c noctalia-shell ipc call plugin:clipper openPanel
|
||||
qs -c noctalia-shell ipc call plugin:clipper closePanel
|
||||
qs -c noctalia-shell ipc call plugin:clipper togglePanel
|
||||
|
||||
# Pinned Items
|
||||
qs -c noctalia-shell ipc call plugin:clipper pinClipboardItem "clip_123"
|
||||
qs -c noctalia-shell ipc call plugin:clipper unpinItem "pinned_456"
|
||||
qs -c noctalia-shell ipc call plugin:clipper copyPinned "pinned_789"
|
||||
|
||||
# ToDo Integration
|
||||
qs -c noctalia-shell ipc call plugin:clipper addSelectionToTodo
|
||||
|
||||
# NoteCards
|
||||
qs -c noctalia-shell ipc call plugin:clipper addNoteCard "Quick note"
|
||||
qs -c noctalia-shell ipc call plugin:clipper exportNoteCard "note_123_abc"
|
||||
qs -c noctalia-shell ipc call plugin:clipper addSelectionToNoteCard # NEW in v2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Reviewed by:** QML Code Reviewer
|
||||
**Date:** 2026-02-05
|
||||
**Follow-up:** Recommended before v2.1 release
|
||||
@@ -0,0 +1,426 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.Noctalia
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
focus: true
|
||||
property var clipboardItem: null
|
||||
property var pluginApi: null
|
||||
property var screen: null
|
||||
property var panelRoot: null
|
||||
property string clipboardId: clipboardItem ? clipboardItem.id : ""
|
||||
property string mime: clipboardItem ? clipboardItem.mime : ""
|
||||
property string preview: clipboardItem ? clipboardItem.preview : ""
|
||||
property string pinnedImageDataUrl: "" // For pinned images - data URL
|
||||
|
||||
// Content type detection
|
||||
readonly property bool isImage: clipboardItem && clipboardItem.isImage
|
||||
readonly property bool isColor: {
|
||||
if (isImage || !preview)
|
||||
return false;
|
||||
const trimmed = preview.trim();
|
||||
return /^#[A-Fa-f0-9]{6}$/.test(trimmed) || /^#[A-Fa-f0-9]{3}$/.test(trimmed) || /^[A-Fa-f0-9]{6}$/.test(trimmed) || /^rgba?\(.*\)$/i.test(trimmed);
|
||||
}
|
||||
readonly property bool isLink: !isImage && !isColor && preview && /^https?:\/\//.test(preview.trim())
|
||||
readonly property bool isCode: !isImage && !isColor && !isLink && preview && (preview.includes("function") || preview.includes("import ") || preview.includes("const ") || preview.includes("let ") || preview.includes("var ") || preview.includes("class ") || preview.includes("def ") || preview.includes("return ") || /^[\{\[\(<]/.test(preview.trim()))
|
||||
readonly property bool isEmoji: {
|
||||
if (isImage || isColor || isLink || isCode || !preview)
|
||||
return false;
|
||||
const trimmed = preview.trim();
|
||||
return trimmed.length <= 4 && trimmed.charCodeAt(0) > 255;
|
||||
}
|
||||
readonly property bool isFile: !isImage && !isColor && !isLink && !isCode && !isEmoji && preview && /^(\/|~|file:\/\/)/.test(preview.trim())
|
||||
readonly property bool isText: !isImage && !isColor && !isLink && !isCode && !isEmoji && !isFile
|
||||
|
||||
// Helper to safely access Color singleton
|
||||
function getColor(propName, fallback) {
|
||||
if (typeof Color !== "undefined" && Color[propName])
|
||||
return Color[propName];
|
||||
return fallback;
|
||||
}
|
||||
|
||||
readonly property string colorValue: {
|
||||
if (!isColor || !preview)
|
||||
return "";
|
||||
const trimmed = preview.trim();
|
||||
if (/^#[A-Fa-f0-9]{3,6}$/.test(trimmed))
|
||||
return trimmed;
|
||||
if (/^[A-Fa-f0-9]{6}$/.test(trimmed))
|
||||
return "#" + trimmed;
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
readonly property string typeLabel: isImage ? "Image" : isColor ? "Color" : isLink ? "Link" : isCode ? "Code" : isEmoji ? "Emoji" : isFile ? "File" : "Text"
|
||||
readonly property string typeIcon: isImage ? "photo" : isColor ? "palette" : isLink ? "link" : isCode ? "code" : isEmoji ? "mood-smile" : isFile ? "file" : "align-left"
|
||||
|
||||
// Default colors per card type
|
||||
readonly property var defaultCardColors: {
|
||||
"Text": {
|
||||
bg: "mOutline",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSurface"
|
||||
},
|
||||
"Image": {
|
||||
bg: "mTertiary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnTertiary"
|
||||
},
|
||||
"Link": {
|
||||
bg: "mPrimary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnPrimary"
|
||||
},
|
||||
"Code": {
|
||||
bg: "mSecondary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSecondary"
|
||||
},
|
||||
"Color": {
|
||||
bg: "mSecondary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSecondary"
|
||||
},
|
||||
"Emoji": {
|
||||
bg: "mHover",
|
||||
separator: "mSurface",
|
||||
fg: "mOnHover"
|
||||
},
|
||||
"File": {
|
||||
bg: "mError",
|
||||
separator: "mSurface",
|
||||
fg: "mOnError"
|
||||
}
|
||||
}
|
||||
|
||||
// Get color setting for current card type
|
||||
function getCardColorSetting(colorType) {
|
||||
const settings = pluginApi?.pluginSettings?.cardColors;
|
||||
const customColors = pluginApi?.pluginSettings?.customColors;
|
||||
const cardType = typeLabel;
|
||||
|
||||
if (settings && settings[cardType] && settings[cardType][colorType]) {
|
||||
const colorKey = settings[cardType][colorType];
|
||||
if (colorKey === "custom" && customColors && customColors[cardType]) {
|
||||
return customColors[cardType][colorType] || "#888888";
|
||||
}
|
||||
return getColor(colorKey, defaultCardColors[cardType]?.[colorType] || "#888888");
|
||||
}
|
||||
|
||||
// Fallback to default
|
||||
const defaultKey = defaultCardColors[cardType]?.[colorType];
|
||||
return getColor(defaultKey, "#888888");
|
||||
}
|
||||
|
||||
// Colors from settings or defaults
|
||||
readonly property color accentColor: getCardColorSetting("bg")
|
||||
readonly property color accentFgColor: getCardColorSetting("fg")
|
||||
readonly property color separatorColor: getCardColorSetting("separator")
|
||||
|
||||
signal clicked
|
||||
signal deleteClicked
|
||||
signal addToTodoClicked
|
||||
signal pinClicked
|
||||
signal rightClicked
|
||||
property bool selected: false
|
||||
property bool enableTodoIntegration: false
|
||||
property bool isPinned: false
|
||||
|
||||
width: 250
|
||||
height: parent.height
|
||||
|
||||
// Get ToDo pages from plugin
|
||||
function getTodoPages() {
|
||||
const todoApi = PluginService.getPluginAPI("todo");
|
||||
if (!todoApi || !todoApi.pluginSettings || !todoApi.pluginSettings.pages) {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: "General"
|
||||
}
|
||||
];
|
||||
}
|
||||
return todoApi.pluginSettings.pages;
|
||||
}
|
||||
|
||||
// Build menu model from ToDo pages
|
||||
function buildTodoMenuModel() {
|
||||
const pages = getTodoPages();
|
||||
const model = [];
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
model.push({
|
||||
"label": pages[i].name,
|
||||
"action": "page-" + pages[i].id,
|
||||
"icon": "checkbox"
|
||||
});
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
// Anchor point for context menu positioning
|
||||
Item {
|
||||
id: menuAnchor
|
||||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
}
|
||||
|
||||
// Context menu for ToDo page selection
|
||||
NPopupContextMenu {
|
||||
id: todoContextMenu
|
||||
screen: root.screen
|
||||
anchorItem: menuAnchor
|
||||
|
||||
onTriggered: action => {
|
||||
todoContextMenu.visible = false;
|
||||
|
||||
if (action.startsWith("page-")) {
|
||||
const pageId = parseInt(action.replace("page-", ""));
|
||||
if (root.preview) {
|
||||
root.pluginApi?.mainInstance?.addTodoWithText(root.preview.substring(0, 200), pageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
// Cleanup when menu closes
|
||||
root.focus = true;
|
||||
if (root.panelRoot && root.panelRoot.activeContextMenu === todoContextMenu) {
|
||||
root.panelRoot.activeContextMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click outside to close menu
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: todoContextMenu.visible
|
||||
z: todoContextMenu.visible ? 999 : -1
|
||||
onClicked: {
|
||||
todoContextMenu.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Body background - Same as accent color
|
||||
color: selected ? Qt.darker(accentColor, 1.1) : (mouseArea.containsMouse ? Qt.lighter(accentColor, 1.1) : accentColor)
|
||||
|
||||
radius: (typeof Style !== "undefined") ? Style.radiusM : 16
|
||||
border.width: 2
|
||||
border.color: accentColor // Border same as background
|
||||
|
||||
// Visual indicator for pinned status (small icon in corner)
|
||||
NIcon {
|
||||
visible: root.isPinned
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 4
|
||||
z: 100
|
||||
icon: "pin-filled"
|
||||
pointSize: 10
|
||||
color: root.accentFgColor
|
||||
opacity: 0.6
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Rectangle {
|
||||
id: headerBar
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 35
|
||||
color: root.accentColor // Header same as background
|
||||
topLeftRadius: (typeof Style !== "undefined") ? Style.radiusM : 16
|
||||
topRightRadius: (typeof Style !== "undefined") ? Style.radiusM : 16
|
||||
bottomLeftRadius: 0
|
||||
bottomRightRadius: 0
|
||||
|
||||
RowLayout {
|
||||
id: headerContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
NIcon {
|
||||
icon: root.typeIcon
|
||||
pointSize: 13
|
||||
color: root.accentFgColor
|
||||
}
|
||||
NText {
|
||||
text: root.typeLabel
|
||||
color: root.accentFgColor
|
||||
font.bold: true
|
||||
}
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NIconButton {
|
||||
id: todoButton
|
||||
visible: root.enableTodoIntegration && !root.isImage
|
||||
icon: "checkbox"
|
||||
tooltipText: pluginApi?.tr("card.add-todo")
|
||||
colorFg: root.accentFgColor
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
onClicked: {
|
||||
// Close any previously open menu
|
||||
if (root.panelRoot && root.panelRoot.activeContextMenu) {
|
||||
root.panelRoot.activeContextMenu.visible = false;
|
||||
}
|
||||
|
||||
// Position anchor at button location
|
||||
const pos = todoButton.mapToItem(root, 0, todoButton.height);
|
||||
menuAnchor.x = pos.x;
|
||||
menuAnchor.y = pos.y;
|
||||
|
||||
// Show menu and register it as active
|
||||
todoContextMenu.model = root.buildTodoMenuModel();
|
||||
todoContextMenu.visible = true;
|
||||
if (root.panelRoot) {
|
||||
root.panelRoot.activeContextMenu = todoContextMenu;
|
||||
}
|
||||
}
|
||||
}
|
||||
NIconButton {
|
||||
visible: !root.isPinned && (pluginApi?.pluginSettings?.pincardsEnabled ?? true) // Hide pin button if pinned or if feature disabled
|
||||
icon: "pin"
|
||||
tooltipText: pluginApi?.tr("card.pin")
|
||||
colorFg: root.accentFgColor
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
onClicked: root.pinClicked()
|
||||
}
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: pluginApi?.tr("card.delete")
|
||||
colorFg: root.accentFgColor
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
onClicked: root.deleteClicked()
|
||||
}
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else {
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 10
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: root.separatorColor
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 8
|
||||
clip: true
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.rightClicked()
|
||||
} else {
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.isColor
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
color: root.colorValue || "transparent"
|
||||
border.width: 1
|
||||
border.color: root.accentFgColor // Use FG color for border contrast
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: !root.isColor && !root.isImage
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
text: root.preview || ""
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
color: root.accentFgColor
|
||||
font.pointSize: 11
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
|
||||
NImageRounded {
|
||||
visible: root.isImage
|
||||
anchors.fill: parent
|
||||
radius: 8
|
||||
imageFillMode: Image.PreserveAspectFit
|
||||
imagePath: {
|
||||
// For pinned images, use data URL directly
|
||||
if (root.pinnedImageDataUrl) {
|
||||
return root.pinnedImageDataUrl;
|
||||
}
|
||||
// For cliphist images, use cache (reactive binding via imageCacheRevision)
|
||||
if (root.isImage && root.pluginApi?.mainInstance) {
|
||||
// Force re-evaluation when cache changes by referencing revision
|
||||
const revision = root.pluginApi.mainInstance.imageCacheRevision;
|
||||
const cache = root.pluginApi.mainInstance.imageCache;
|
||||
return cache[root.clipboardId] || "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
Component.onCompleted: {
|
||||
// Only decode if not pinned (pinned images have data URL already)
|
||||
if (!root.pinnedImageDataUrl && root.isImage && root.clipboardId && root.pluginApi?.mainInstance) {
|
||||
root.pluginApi.mainInstance.decodeToDataUrl(root.clipboardId, root.mime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onDestruction: {
|
||||
if (todoContextMenu && todoContextMenu.visible) {
|
||||
todoContextMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
property var pluginApi: null
|
||||
|
||||
icon: "clipboard-data"
|
||||
tooltipText: pluginApi?.tr("bar.tooltip")
|
||||
|
||||
onClicked: {
|
||||
if (pluginApi) {
|
||||
pluginApi.togglePanel(screen);
|
||||
}
|
||||
}
|
||||
|
||||
onRightClicked: {
|
||||
if (pluginApi && pluginApi.manifest) {
|
||||
BarService.openPluginSettings(screen, pluginApi.manifest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
// Properties
|
||||
property var pluginApi: null
|
||||
property var note: null
|
||||
property int noteIndex: 0
|
||||
|
||||
// Color schemes
|
||||
property var colorSchemes: ({
|
||||
"yellow": {
|
||||
bg: "#FFF9C4",
|
||||
fg: "#000000",
|
||||
header: "#FDD835"
|
||||
},
|
||||
"pink": {
|
||||
bg: "#FCE4EC",
|
||||
fg: "#000000",
|
||||
header: "#F06292"
|
||||
},
|
||||
"blue": {
|
||||
bg: "#E3F2FD",
|
||||
fg: "#000000",
|
||||
header: "#42A5F5"
|
||||
},
|
||||
"green": {
|
||||
bg: "#E8F5E9",
|
||||
fg: "#000000",
|
||||
header: "#66BB6A"
|
||||
},
|
||||
"purple": {
|
||||
bg: "#F3E5F5",
|
||||
fg: "#000000",
|
||||
header: "#AB47BC"
|
||||
}
|
||||
})
|
||||
|
||||
// Constants for sizing
|
||||
readonly property int minHeight: 200
|
||||
readonly property int maxHeight: 600
|
||||
readonly property int headerHeight: 40
|
||||
readonly property int margins: 24
|
||||
|
||||
// Position and size from note data
|
||||
x: note ? note.x : 0
|
||||
y: note ? note.y : 0
|
||||
width: note ? note.width : 350
|
||||
height: note ? note.height : minHeight
|
||||
z: note ? note.zIndex : 0
|
||||
|
||||
// Color from note data
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.bg : "#FFF9C4";
|
||||
}
|
||||
border.color: Color.mOnSurfaceVariant
|
||||
border.width: 1
|
||||
radius: Style.radiusM
|
||||
|
||||
// Main layout
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
id: headerBar
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.headerHeight
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.header : "#FDD835";
|
||||
}
|
||||
topLeftRadius: Style.radiusM
|
||||
topRightRadius: Style.radiusM
|
||||
bottomLeftRadius: 0
|
||||
bottomRightRadius: 0
|
||||
|
||||
RowLayout {
|
||||
id: headerContent
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 6
|
||||
spacing: 10
|
||||
z: 1
|
||||
|
||||
// Icon - DRAG HANDLE
|
||||
Item {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.fillHeight: true
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: "note"
|
||||
pointSize: 15
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.SizeAllCursor
|
||||
|
||||
drag.target: root
|
||||
drag.axis: Drag.XAndYAxis
|
||||
drag.minimumX: 0
|
||||
drag.maximumX: root.parent ? (root.parent.width - root.width) : 1200
|
||||
drag.minimumY: 0
|
||||
drag.maximumY: root.parent ? (root.parent.height - root.height) : 700
|
||||
|
||||
onPressed: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.bringNoteToFront(root.note.id);
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.updateNoteCard(root.note.id, {
|
||||
x: root.x,
|
||||
y: root.y
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Title
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumWidth: 100
|
||||
|
||||
TextInput {
|
||||
id: titleInput
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 4
|
||||
anchors.rightMargin: 4
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
font.pixelSize: 14
|
||||
font.bold: false
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: pluginApi?.tr("notecards.untitled-placeholder")
|
||||
color: parent.color
|
||||
opacity: 0.5
|
||||
visible: titleInput.text.length === 0
|
||||
font: titleInput.font
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (note) {
|
||||
text = note.title || "";
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
root.syncChanges();
|
||||
titleInput.focus = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: note && note.isPrivate ? "eye-off" : "eye"
|
||||
tooltipText: pluginApi?.tr("notecards.toggle-privacy-mode")
|
||||
colorFg: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
|
||||
onClicked: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.updateNoteCard(root.note.id, {
|
||||
isPrivate: !(root.note && root.note.isPrivate)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "palette"
|
||||
tooltipText: pluginApi?.tr("notecards.change-color")
|
||||
colorFg: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
|
||||
onClicked: {
|
||||
const colors = ["yellow", "pink", "blue", "green", "purple"];
|
||||
const noteColor = (root.note && root.note.color) ? root.note.color : "yellow";
|
||||
const currentIndex = colors.indexOf(noteColor);
|
||||
const nextIndex = (currentIndex + 1) % colors.length;
|
||||
const nextColor = colors[nextIndex];
|
||||
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.updateNoteCard(root.note.id, {
|
||||
color: nextColor
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "file-export"
|
||||
tooltipText: pluginApi?.tr("notecards.export")
|
||||
colorFg: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
|
||||
onClicked: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.exportNoteCard(root.note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "trash"
|
||||
tooltipText: pluginApi?.tr("notecards.delete")
|
||||
colorFg: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
colorBg: "transparent"
|
||||
colorBgHover: Qt.rgba(0, 0, 0, 0.1)
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
|
||||
onClicked: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.deleteNoteCard(root.note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - 10
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
height: 1
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "transparent"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.header : "#FDD835";
|
||||
}
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content area with ScrollView
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 12
|
||||
clip: true
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
|
||||
TextArea {
|
||||
id: textArea
|
||||
width: parent.width
|
||||
wrapMode: TextArea.Wrap
|
||||
selectByMouse: true
|
||||
color: {
|
||||
if (note.isPrivate && !textArea.activeFocus) {
|
||||
return "transparent"
|
||||
}
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
font.pixelSize: 14
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (note) {
|
||||
text = note.content || "";
|
||||
}
|
||||
// Check if we need to expand card on load
|
||||
Qt.callLater(checkAndExpandHeight);
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
if (note) {
|
||||
note.content = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
enabled: false
|
||||
opacity: 1.0
|
||||
visible: note.isPrivate && !textArea.activeFocus
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
|
||||
NIcon {
|
||||
anchors.centerIn: parent
|
||||
icon: "password"
|
||||
pointSize: 45
|
||||
color: {
|
||||
const noteColor = note ? note.color : "yellow";
|
||||
const scheme = colorSchemes[noteColor];
|
||||
return scheme ? scheme.fg : "#000000";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if card needs to be expanded to fit content
|
||||
function checkAndExpandHeight() {
|
||||
if (!textArea || !note)
|
||||
return;
|
||||
|
||||
const contentHeight = textArea.contentHeight;
|
||||
const availableHeight = root.height - root.headerHeight - root.margins - 1; // 1 = separator
|
||||
|
||||
// If content doesn't fit, expand card
|
||||
if (contentHeight > availableHeight) {
|
||||
let newHeight = root.headerHeight + root.margins + contentHeight + 1;
|
||||
newHeight = Math.min(newHeight, root.maxHeight);
|
||||
|
||||
if (newHeight !== root.height && root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.updateNoteCard(root.note.id, {
|
||||
height: newHeight
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncChanges() {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance && note) {
|
||||
root.pluginApi.mainInstance.updateNoteCard(note.id, {
|
||||
title: titleInput.text,
|
||||
content: textArea.text
|
||||
});
|
||||
}
|
||||
}
|
||||
Component.onDestruction: {
|
||||
root.syncChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
// Note card selector wrapper - prepares menu items and handles selection
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property string selectedText: ""
|
||||
property var noteCards: []
|
||||
property var selectionMenu: null
|
||||
|
||||
signal noteSelected(string noteId, string noteTitle)
|
||||
signal createNewNote
|
||||
|
||||
function buildMenuModel() {
|
||||
const model = [];
|
||||
|
||||
// First option: Create new note
|
||||
model.push({
|
||||
"label": pluginApi?.tr("notecards.create-note"),
|
||||
"action": "create-new",
|
||||
"icon": "add"
|
||||
});
|
||||
|
||||
// Add existing notes
|
||||
for (let i = 0; i < noteCards.length; i++) {
|
||||
const note = noteCards[i];
|
||||
model.push({
|
||||
"label": note.title || pluginApi?.tr("notecards.untitled-placeholder"),
|
||||
"action": "note-" + note.id,
|
||||
"icon": "note",
|
||||
"noteId": note.id
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
function show(text, notes) {
|
||||
selectedText = text || "";
|
||||
noteCards = notes || [];
|
||||
|
||||
if (selectionMenu) {
|
||||
const menuItems = buildMenuModel();
|
||||
selectionMenu.show(menuItems);
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemSelected(action) {
|
||||
if (action === "create-new") {
|
||||
root.createNewNote();
|
||||
} else if (action.startsWith("note-")) {
|
||||
const noteId = action.replace("note-", "");
|
||||
const note = noteCards.find(n => n.id === noteId);
|
||||
root.noteSelected(noteId, note ? note.title : pluginApi?.tr("notecards.untitled-placeholder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property var screen: null
|
||||
|
||||
// Function to sync all notecard changes before saving
|
||||
function syncAllChanges() {
|
||||
for (let i = 0; i < noteCardsRepeater.count; i++) {
|
||||
const card = noteCardsRepeater.itemAt(i);
|
||||
if (card && card.syncChanges) {
|
||||
card.syncChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Background MouseArea - ALWAYS closes panel on click
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
|
||||
onClicked: {
|
||||
const closeButtonOn = root.pluginApi?.pluginSettings?.showCloseButton ?? false;
|
||||
if (!closeButtonOn && root.pluginApi && root.screen) {
|
||||
root.pluginApi.closePanel(root.screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state UI (shown when no notes)
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: 400
|
||||
height: 200
|
||||
visible: !(root.pluginApi && root.pluginApi.mainInstance && root.pluginApi.mainInstance.noteCards) || root.pluginApi.mainInstance.noteCards.length === 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
|
||||
NIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
icon: "notes"
|
||||
pointSize: 64
|
||||
color: Color.mOnSurfaceVariant
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: pluginApi?.tr("notecards.empty-state")
|
||||
font.pointSize: Style.fontSizeL
|
||||
font.bold: true
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: pluginApi?.tr("notecards.empty-hint")
|
||||
font.pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: 16
|
||||
text: pluginApi?.tr("notecards.create-note")
|
||||
icon: "add"
|
||||
|
||||
onClicked: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
root.pluginApi.mainInstance.createNoteCard("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use ListModel to avoid binding loop
|
||||
ListModel {
|
||||
id: noteCardsModel
|
||||
}
|
||||
|
||||
property int lastRevision: -1
|
||||
|
||||
// Manual update function
|
||||
function updateNoteCardsModel() {
|
||||
noteCardsModel.clear();
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
const notes = root.pluginApi.mainInstance.noteCards || [];
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
noteCardsModel.append({
|
||||
"noteData": notes[i],
|
||||
"noteIdx": i
|
||||
});
|
||||
}
|
||||
lastRevision = root.pluginApi.mainInstance.noteCardsRevision || 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to check for changes
|
||||
Timer {
|
||||
id: revisionTimer
|
||||
interval: 200
|
||||
running: root.visible
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
const currentRevision = root.pluginApi.mainInstance.noteCardsRevision || 0;
|
||||
if (currentRevision !== root.lastRevision) {
|
||||
root.updateNoteCardsModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateNoteCardsModel();
|
||||
}
|
||||
|
||||
// Repeater for note cards
|
||||
Repeater {
|
||||
id: noteCardsRepeater
|
||||
model: noteCardsModel
|
||||
|
||||
NoteCard {
|
||||
pluginApi: root.pluginApi
|
||||
note: noteData
|
||||
noteIndex: noteIdx
|
||||
z: 1 // Above background MouseArea
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 16
|
||||
width: contentRow.width + 16
|
||||
height: 40
|
||||
color: Color.mSurfaceVariant
|
||||
border.color: Color.mOnSurfaceVariant
|
||||
border.width: 1
|
||||
radius: Style.radiusM
|
||||
visible: (root.pluginApi && root.pluginApi.mainInstance && root.pluginApi.mainInstance.noteCards) ? root.pluginApi.mainInstance.noteCards.length > 0 : false
|
||||
z: 2 // Above everything
|
||||
|
||||
RowLayout {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
// Add note button
|
||||
NIconButton {
|
||||
width: 28
|
||||
height: 28
|
||||
icon: "add"
|
||||
tooltipText: pluginApi?.tr("notecards.create-note")
|
||||
colorBg: Color.mPrimary
|
||||
colorFg: Color.mOnPrimary
|
||||
colorBgHover: Qt.lighter(Color.mPrimary, 1.2)
|
||||
colorFgHover: Color.mOnPrimary
|
||||
|
||||
onClicked: {
|
||||
if (root.pluginApi && root.pluginApi.mainInstance) {
|
||||
const count = root.pluginApi.mainInstance.noteCards ? root.pluginApi.mainInstance.noteCards.length : 0;
|
||||
const max = root.pluginApi.mainInstance.maxNoteCards || 20;
|
||||
|
||||
if (count >= max) {
|
||||
ToastService.showWarning(pluginApi?.tr("toast.max-notes").replace("{max}", max));
|
||||
} else {
|
||||
root.pluginApi.mainInstance.createNoteCard("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical separator
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 24
|
||||
color: Color.mOnSurfaceVariant
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
// Note icon
|
||||
NIcon {
|
||||
icon: "note"
|
||||
pointSize: 16
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
// Count text
|
||||
NText {
|
||||
text: {
|
||||
const count = (root.pluginApi && root.pluginApi.mainInstance && root.pluginApi.mainInstance.noteCards) ? root.pluginApi.mainInstance.noteCards.length : 0;
|
||||
const max = (root.pluginApi && root.pluginApi.mainInstance) ? (root.pluginApi.mainInstance.maxNoteCards || 20) : 20;
|
||||
return count + " / " + max;
|
||||
}
|
||||
font.pointSize: Style.fontSizeM
|
||||
font.bold: true
|
||||
color: {
|
||||
const count = (root.pluginApi && root.pluginApi.mainInstance && root.pluginApi.mainInstance.noteCards) ? root.pluginApi.mainInstance.noteCards.length : 0;
|
||||
const max = (root.pluginApi && root.pluginApi.mainInstance) ? (root.pluginApi.mainInstance.maxNoteCards || 20) : 20;
|
||||
return count >= max ? Color.mError : Color.mOnSurfaceVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Component.onDestruction: {
|
||||
revisionTimer.stop();
|
||||
noteCardsModel.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
# Clipper - Advanced Clipboard Manager for Noctalia Shell
|
||||
|
||||
**Version 2.3.0** - A powerful clipboard history manager with persistent pinned items, NoteCards sticky notes, seamless ToDo integration and auto-paste.
|
||||
|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
### ⚙️ Control Center Shortcut
|
||||
- **Control Center Shortcut**: Access Clipper in Control Center shortcut instead to save widget spaces in your bar
|
||||
|
||||
### 📋 Clipboard Management
|
||||
- **Unlimited History**: Access your entire clipboard history powered by `cliphist`
|
||||
- **Smart Filtering**: Filter by type (text, images, colors, links, code, emoji, files)
|
||||
- **Quick Search**: Real-time search through clipboard items
|
||||
- **Persistent Pinned Items**: Pin important items that survive across sessions
|
||||
- **Rich Preview**: Image previews, color swatches, and formatted text
|
||||
|
||||
### 📝 NoteCards (Sticky Notes)
|
||||
Quick capture and organize your thoughts with persistent sticky notes:
|
||||
|
||||

|
||||
|
||||
- **Individual JSON Storage**: Each notecard stored separately for data safety
|
||||
- **Editable Titles**: Click title to edit, press Enter to save
|
||||
- **Color Coding**: 8 preset colors for visual organization
|
||||
- **Auto-saving Content**: Content saves when panel closes
|
||||
- **Export to TXT**: Export notecards to `~/Documents/`
|
||||
- **Drag to Reorder**: Organize cards by dragging (visual feedback)
|
||||
- **Add Selection**: Capture selected text directly to notecard via keybind
|
||||
|
||||
### ✅ ToDo Integration
|
||||
Seamless integration with Noctalia ToDo plugin:
|
||||
|
||||

|
||||
|
||||
- **Smart Context Menu**: Right-click clipboard items to add to specific ToDo pages
|
||||
- **Selection to ToDo**: Capture selected text directly to ToDo via keybind
|
||||
- **Multi-Page Support**: Choose target ToDo page from visual menu
|
||||
- **Auto-Copy**: Selected text automatically copied to clipboard
|
||||
|
||||
### 🎨 Visual Customization
|
||||
- **Per-Type Color Schemes**: Customize colors for each card type separately
|
||||
- Text cards, Image cards, Color cards, Link cards, etc.
|
||||
- **Three Color Properties**: Background, Separator, Foreground (text/icons)
|
||||
- **Live Preview**: See changes instantly in settings panel
|
||||
- **Reset to Defaults**: One-click restore to default theme
|
||||
|
||||
- ### ⚡ Auto-Paste
|
||||
- **Toggle in Settings → Auto-Paste**: After selecting a clipboard item, content is automatically pasted into the focused window
|
||||
- **Right-Click Only mode**: Left-click copies normally; right-click copies and pastes
|
||||
- **Paste Delay slider**: Tune the delay (100–1000 ms) for compositor focus timing
|
||||
- Requires `wtype` (`sudo pacman -S wtype`); settings show a warning when not installed
|
||||
|
||||
## 🎬 Video Demonstrations
|
||||
|
||||
### NoteCards in Action
|
||||
Creating notecards, editing titles and content, color coding, and exporting:
|
||||
|
||||

|
||||
|
||||
### ToDo Integration
|
||||
Adding clipboard items to ToDo, selection to ToDo workflow, and multi-page management:
|
||||
|
||||

|
||||
## 🚀 Installation
|
||||
|
||||
### From Noctalia Plugin Manager (Recommended)
|
||||
1. Open Noctalia Settings → Plugins
|
||||
2. Search for "Clipper"
|
||||
3. Click Install
|
||||
4. Reload Noctalia Shell
|
||||
|
||||
### Manual Installation
|
||||
```bash
|
||||
# Clone repository
|
||||
cd ~/.config/noctalia/plugins/
|
||||
git clone https://github.com/blackbartblues/noctalia-clipper clipper
|
||||
|
||||
# Reload Noctalia
|
||||
qs -c noctalia-shell reload
|
||||
```
|
||||
|
||||
## ⌨️ Keybind Setup
|
||||
|
||||
### Hyprland Configuration
|
||||
|
||||
Add to `~/.config/hypr/keybind.conf`:
|
||||
|
||||
```conf
|
||||
# Clipper - Clipboard Manager
|
||||
bindr = SUPER, V, exec, qs -c noctalia-shell ipc call plugin:clipper toggle
|
||||
|
||||
# Selection to ToDo (chord: Super+V, then C)
|
||||
binds = SUPER_L, V&C, exec, qs -c noctalia-shell ipc call plugin:clipper addSelectionToTodo
|
||||
|
||||
# Selection to NoteCard (chord: Super+V, then X)
|
||||
binds = SUPER_L, V&X, exec, qs -c noctalia-shell ipc call plugin:clipper addSelectionToNoteCard
|
||||
```
|
||||
|
||||
**Chord Keybinds Explained:**
|
||||
- Press `Super+V` (don't release Super)
|
||||
- While holding Super, press `C` → adds selection to ToDo
|
||||
- While holding Super, press `X` → adds selection to NoteCard
|
||||
|
||||
## 🎮 Usage
|
||||
|
||||
### Basic Operations
|
||||
|
||||
**Toggle Panel:**
|
||||
```bash
|
||||
qs -c noctalia-shell ipc call plugin:clipper toggle
|
||||
```
|
||||
|
||||
**Pin/Unpin Item:**
|
||||
- Click pin icon on clipboard card
|
||||
- Or use context menu (right-click)
|
||||
|
||||
**Add to ToDo:**
|
||||
- Right-click clipboard item → "Add to ToDo"
|
||||
- Select target ToDo page from menu
|
||||
|
||||
### NoteCards
|
||||
|
||||
**Create NoteCard:**
|
||||
- Click "+ Create Note" button in NoteCards panel
|
||||
- Or via IPC:
|
||||
```bash
|
||||
qs -c noctalia-shell ipc call plugin:clipper addNoteCard "Quick note"
|
||||
```
|
||||
|
||||
**Edit NoteCard:**
|
||||
- Click title to edit, press Enter to save
|
||||
- Type in content area (auto-saves on panel close)
|
||||
|
||||
**Change Color:**
|
||||
- Right-click notecard → "Change Color"
|
||||
- Select from 8 preset colors
|
||||
|
||||
**Export NoteCard:**
|
||||
- Right-click notecard → "Export to .txt"
|
||||
- Saved to `~/Documents/note_TIMESTAMP.txt`
|
||||
|
||||
**Add Selection to NoteCard:**
|
||||
1. Select text in any application
|
||||
2. Press keybind (e.g., `Super+V, X`)
|
||||
3. Choose existing notecard or create new one
|
||||
4. Text added as bullet point
|
||||
|
||||
### Advanced IPC Commands
|
||||
|
||||
```bash
|
||||
# Panel Management
|
||||
qs -c noctalia-shell ipc call plugin:clipper openPanel
|
||||
qs -c noctalia-shell ipc call plugin:clipper closePanel
|
||||
qs -c noctalia-shell ipc call plugin:clipper togglePanel
|
||||
|
||||
# Pinned Items
|
||||
qs -c noctalia-shell ipc call plugin:clipper pinClipboardItem "123456"
|
||||
qs -c noctalia-shell ipc call plugin:clipper unpinItem "pinned-123"
|
||||
qs -c noctalia-shell ipc call plugin:clipper copyPinned "pinned-123"
|
||||
|
||||
# NoteCards
|
||||
qs -c noctalia-shell ipc call plugin:clipper addNoteCard "Quick note"
|
||||
qs -c noctalia-shell ipc call plugin:clipper exportNoteCard "note_abc123"
|
||||
qs -c noctalia-shell ipc call plugin:clipper addSelectionToNoteCard
|
||||
|
||||
# ToDo Integration
|
||||
qs -c noctalia-shell ipc call plugin:clipper addSelectionToTodo
|
||||
```
|
||||
|
||||
## ⚙️ Settings
|
||||
|
||||
### General
|
||||
- **ToDo Plugin Integration**: Enable/disable ToDo integration (auto-detects if plugin installed)
|
||||
|
||||
### NoteCards
|
||||
- **Enable NoteCards**: Toggle NoteCards panel visibility
|
||||
- **Default Note Color**: Choose default color for new notecards
|
||||
- **Current Notes Count**: Shows number of stored notecards
|
||||
- **Clear All Notes**: One-click removal of all notecards
|
||||
|
||||
### Appearance
|
||||
Customize colors for each card type:
|
||||
- **Card Type Selector**: Choose which type to customize (Text, Image, Color, Link, Code, Emoji, File)
|
||||
- **Live Preview**: See changes in real-time
|
||||
- **Background Color**: Card background
|
||||
- **Separator Color**: Line between header and content
|
||||
- **Foreground Color**: Text, icons, and content color
|
||||
- **Reset to Defaults**: Restore default color scheme
|
||||
|
||||
## 📁 Data Storage
|
||||
|
||||
```
|
||||
~/.config/noctalia/plugins/clipper/
|
||||
├── settings.json # Plugin settings and color customization
|
||||
├── pinned.json # Persistent pinned items
|
||||
└── notecards/
|
||||
├── note_abc123.json # Individual notecard files
|
||||
├── note_def456.json
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### Dependencies
|
||||
- **cliphist**: Clipboard history backend (required)
|
||||
- **wl-clipboard**: Wayland clipboard utilities (required)
|
||||
- **Noctalia ToDo Plugin**: For ToDo integration (optional)
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
# Arch Linux
|
||||
sudo pacman -S cliphist wl-clipboard
|
||||
|
||||
# Or use your distribution's package manager
|
||||
```
|
||||
|
||||
### Architecture
|
||||
- **Main.qml**: Core logic, IPC handlers, data management
|
||||
- **Panel.qml**: Main clipboard history panel UI
|
||||
- **NoteCardsPanel.qml**: Sticky notes panel UI
|
||||
- **TodoPageSelector.qml**: ToDo page selection logic
|
||||
- **NoteCardSelector.qml**: Notecard selection logic
|
||||
- **SelectionContextMenu.qml**: Universal context menu for selections
|
||||
- **ClipboardCard.qml**: Individual clipboard item card
|
||||
- **NoteCard.qml**: Individual notecard component
|
||||
- **Settings.qml**: Settings panel UI
|
||||
- **BarWidget.qml**: Bar widget button
|
||||
|
||||
### Translation System
|
||||
- **16 Languages Supported**: en, de, es, fr, hu, it, ja, ko-KR, nl, pl, pt, ru, sv, tr, uk-UA, zh-CN, zh-TW
|
||||
- **100% Coverage**: All user-facing strings translated
|
||||
- **Synchronized Structure**: All translation files have identical keys
|
||||
|
||||
### Memory Management
|
||||
- **Automatic Cleanup**: All components properly destroyed on exit
|
||||
- **Process Termination**: Background processes stopped on destruction
|
||||
- **Timer Management**: All timers stopped when not needed
|
||||
- **No Memory Leaks**: Tested with 500+ operations
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Clipboard history not working
|
||||
```bash
|
||||
# Ensure cliphist is running
|
||||
pkill -9 wl-paste; wl-paste --watch cliphist store &
|
||||
```
|
||||
|
||||
### Pinned items not persisting
|
||||
- Check `~/.config/noctalia/plugins/clipper/pinned.json` exists
|
||||
- Verify file permissions (should be readable/writable by user)
|
||||
|
||||
### NoteCards not saving
|
||||
- Check `~/.config/noctalia/plugins/clipper/notecards/` directory exists
|
||||
- Verify write permissions
|
||||
|
||||
### ToDo integration not working
|
||||
1. Check if ToDo plugin is installed and enabled
|
||||
2. Verify settings: Enable "ToDo Plugin Integration"
|
||||
3. Check `~/.config/noctalia/plugins.json` contains ToDo plugin
|
||||
|
||||
### Context menu not appearing
|
||||
- Ensure screen is correctly detected
|
||||
- Check logs: `journalctl --user -u plasma-qs@noctalia-shell.service -f`
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history.
|
||||
|
||||
### v2.2.0 (2026-03-11) - Current Release
|
||||
|
||||
**Bug Fixes:**
|
||||
- 🐞 Clear All Notes doesn't clear notes when reopen plugin panel fix
|
||||
- 🐞 Square Panel Corners at the bottom corners fix
|
||||
- 🐞 Export a Note multiple times and not deleting it fix
|
||||
- 🐞 Wait until user press Apply button to apply the settings
|
||||
- 🐞 clicking the NoteCards panel would close the plugin panel even when the Close Button was enabled. The panel now only closes on background click when the Close Button is disabled.
|
||||
|
||||
**Improvements:**
|
||||
- 🎨 Panel will now attach to bar instead of being separate
|
||||
- 🎨 Decrease panel Width and Height for a cleaner look
|
||||
- 🎨 Added a separator between Note Card and Pinned Itemnhanced visual customization
|
||||
- 🎨 Move Panel Close Button to the top right corner instead of being next to Open Plugin Settings
|
||||
- 🎨 Added active ring, badge counts for filter buttons
|
||||
- 🔧 Remaps keybindings using Alt + 1 to 8 instead of Alt + 0 to 7
|
||||
- 🔧 Mouse Wheel Scroll left and right clipboards support
|
||||
|
||||
**Technical:**
|
||||
- 📦 Zero console.log statements in production
|
||||
- 🧹 No internal IPC calls (only external API)
|
||||
- 🔒 Secure command execution (no injection vulnerabilities)
|
||||
- ✅ Tested: 500+ operations, no memory growth
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions welcome! Please:
|
||||
1. Follow QML code style guidelines
|
||||
2. Use `pluginApi?.tr()` for all user-facing strings
|
||||
3. Test memory management (Component.onDestruction)
|
||||
4. Ensure all translation files are synchronized
|
||||
5. No console.log in production code
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## 👤 Author
|
||||
|
||||
**blackbartblues**
|
||||
- GitHub: [@blackbartblues](https://github.com/blackbartblues)
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- Noctalia Shell team for the amazing desktop environment
|
||||
- cliphist for robust clipboard history
|
||||
- Community contributors and testers
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for Noctalia Shell**
|
||||
@@ -0,0 +1,151 @@
|
||||
# Clipper v2.0.0 Release Notes
|
||||
|
||||
**Release Date**: 2026-02-05
|
||||
|
||||
## 🎉 Major Features
|
||||
|
||||
### 📝 NoteCards (Sticky Notes)
|
||||
Brand new sticky notes system for quick note-taking:
|
||||
- Individual JSON storage for each notecard
|
||||
- Editable titles and auto-saving content
|
||||
- 8 color presets for organization
|
||||
- Export to TXT files
|
||||
- Add selected text directly to notecards via keybind
|
||||
- Drag to reorder (visual feedback)
|
||||
|
||||
**Demo**: [Notecard Preview.mp4](Assets/Notecard%20Preview.mp4)
|
||||
|
||||
### ✅ Enhanced ToDo Integration
|
||||
Completely revamped ToDo integration:
|
||||
- Selection-to-ToDo with visual page selector
|
||||
- Context menu for adding clipboard items to specific pages
|
||||
- Fixed pageId type handling (int instead of string)
|
||||
- Proper active selector routing
|
||||
|
||||
**Demo**: [ToDo Preview.mp4](Assets/ToDo%20Preview.mp4)
|
||||
|
||||
### 🌍 Complete Translation Coverage
|
||||
- 16 languages fully supported
|
||||
- 30+ toast messages translated
|
||||
- All UI strings use `pluginApi?.tr()`
|
||||
- Synchronized structure across all language files
|
||||
|
||||
## 🔧 Bug Fixes
|
||||
|
||||
### Critical Fixes
|
||||
- ✅ Fixed `addSelectionToTodo` not adding items to correct page
|
||||
- ✅ Fixed pageId type (string → int) causing ToDo filtering issues
|
||||
- ✅ Fixed `handleTodoPageSelected` syntax error (missing closing brace)
|
||||
- ✅ Fixed selector routing with `activeSelector` property
|
||||
|
||||
### High Priority Fixes
|
||||
- ✅ Fixed timer memory leak in NoteCardsPanel.qml
|
||||
- ✅ Fixed all hardcoded strings (now use translations)
|
||||
- ✅ Fixed `appendTextToNoteCard` mutation (now immutable)
|
||||
|
||||
### Medium Priority Fixes
|
||||
- ✅ Added Component.onDestruction cleanup to ClipboardCard.qml
|
||||
- ✅ Removed all console.log debug statements
|
||||
- ✅ Removed debug stack traces from Noctalia core
|
||||
|
||||
## 🎨 Improvements
|
||||
|
||||
### Architecture
|
||||
- Immutable data patterns throughout codebase
|
||||
- Proper memory cleanup (timers, processes, connections)
|
||||
- Active selector routing for context menus
|
||||
- No internal IPC calls (only external API)
|
||||
|
||||
### User Experience
|
||||
- Universal SelectionContextMenu component
|
||||
- Smooth notecard interactions
|
||||
- Visual feedback for all operations
|
||||
- Consistent color scheme customization
|
||||
|
||||
### Performance
|
||||
- Zero memory leaks (tested with 500+ operations)
|
||||
- Efficient ListModel-based rendering
|
||||
- Proper garbage collection
|
||||
|
||||
## 📦 Technical Details
|
||||
|
||||
### Files Changed
|
||||
- Main.qml (1176 lines) - Core logic and IPC handlers
|
||||
- TodoPageSelector.qml - Fixed pageId type
|
||||
- SelectionContextMenu.qml - Universal context menu
|
||||
- Panel.qml, NoteCardsPanel.qml - UI improvements
|
||||
- Settings.qml - Enhanced configuration
|
||||
- i18n/*.json - All 16 files synchronized
|
||||
|
||||
### New Dependencies
|
||||
None - still uses cliphist and wl-clipboard
|
||||
|
||||
### Breaking Changes
|
||||
None - fully backward compatible with v1.x settings
|
||||
|
||||
## 🚀 Upgrade Instructions
|
||||
|
||||
### From v1.x
|
||||
1. **Backup your data** (optional, but recommended):
|
||||
```bash
|
||||
cp -r ~/.config/noctalia/plugins/clipper/pinned.json ~/clipper-backup.json
|
||||
```
|
||||
|
||||
2. **Update plugin**:
|
||||
- Via Plugin Manager: Update button in Noctalia Settings
|
||||
- Via Git:
|
||||
```bash
|
||||
cd ~/.config/noctalia/plugins/clipper
|
||||
git pull
|
||||
```
|
||||
|
||||
3. **Reload Noctalia**:
|
||||
```bash
|
||||
qs -c noctalia-shell reload
|
||||
```
|
||||
|
||||
4. **No migration needed** - all settings and pinned items preserved!
|
||||
|
||||
### Fresh Install
|
||||
See [README.md](README.md) for installation instructions.
|
||||
|
||||
## 🎬 Media
|
||||
|
||||
### Screenshots
|
||||

|
||||
|
||||
### Videos
|
||||
- [Notecard Preview.mp4](Assets/Notecard%20Preview.mp4)
|
||||
- [ToDo Preview.mp4](Assets/ToDo%20Preview.mp4)
|
||||
|
||||
## 📝 Full Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for complete version history.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
Special thanks to:
|
||||
- Code reviewers who identified all issues
|
||||
- Memory leak testers
|
||||
- Translation contributors
|
||||
- Noctalia Shell team
|
||||
|
||||
## 🐛 Known Issues
|
||||
|
||||
None currently! 🎉
|
||||
|
||||
If you find any bugs, please report at:
|
||||
https://github.com/blackbartblues/noctalia-clipper/issues
|
||||
|
||||
## 💡 What's Next?
|
||||
|
||||
Planned for v2.1.0:
|
||||
- Notecard categories/folders
|
||||
- Search within notecards
|
||||
- Rich text formatting
|
||||
- Notecard templates
|
||||
- Cloud sync (optional)
|
||||
|
||||
---
|
||||
|
||||
**Enjoy Clipper v2.0.0!** 🚀
|
||||
@@ -0,0 +1,120 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
// Universal selection context menu that appears at cursor position
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
property var pluginApi: null
|
||||
property var menuItems: []
|
||||
|
||||
signal itemSelected(string action)
|
||||
signal cancelled
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "noctalia-selection-menu-" + (screen?.name || "unknown")
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
property int cursorX: 0
|
||||
property int cursorY: 0
|
||||
|
||||
function show(items) {
|
||||
menuItems = items || [];
|
||||
visible = true;
|
||||
getCursorPositionProcess.running = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
contextMenu.visible = false;
|
||||
}
|
||||
|
||||
// Get cursor position from hyprctl
|
||||
Process {
|
||||
id: getCursorPositionProcess
|
||||
command: ["hyprctl", "cursorpos", "-j"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: cursorStdout
|
||||
}
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0) {
|
||||
try {
|
||||
const data = JSON.parse(cursorStdout.text);
|
||||
root.cursorX = data.x || 0;
|
||||
root.cursorY = data.y || 0;
|
||||
|
||||
// Position menu at cursor
|
||||
anchorPoint.x = root.cursorX;
|
||||
anchorPoint.y = root.cursorY;
|
||||
contextMenu.model = root.menuItems;
|
||||
contextMenu.anchorItem = anchorPoint;
|
||||
contextMenu.visible = true;
|
||||
} catch (e) {
|
||||
ToastService.showError(pluginApi?.tr("toast.failed-to-get-cursor-position"));
|
||||
root.close();
|
||||
}
|
||||
} else {
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
visible: false
|
||||
screen: root.screen
|
||||
minWidth: 200
|
||||
|
||||
onTriggered: (action, item) => {
|
||||
root.itemSelected(action);
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor point for menu positioning
|
||||
Item {
|
||||
id: anchorPoint
|
||||
width: 1
|
||||
height: 1
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
|
||||
// Fullscreen mouse area to capture outside clicks
|
||||
MouseArea {
|
||||
id: mouseCapture
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
|
||||
onClicked: mouse => {
|
||||
root.cancelled();
|
||||
root.close();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (getCursorPositionProcess.running) {
|
||||
getCursorPositionProcess.terminate();
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,974 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
// Live preview + revert-on-cancel pattern (approved deviation from AGENTS.md edit-copy).
|
||||
// User-approved 2026-04-20. Settings changes apply visually in real time to Panel/BarWidget
|
||||
// but are only persisted to disk when the shell calls saveSettings() (Apply button).
|
||||
// Closing without Apply restores the snapshot taken on open.
|
||||
property var _snapshot: null
|
||||
property bool _applied: false
|
||||
|
||||
function _applyPreview(key, value) {
|
||||
if (!pluginApi) return;
|
||||
var patch = {};
|
||||
patch[key] = value;
|
||||
pluginApi.pluginSettings = Object.assign({}, pluginApi.pluginSettings, patch);
|
||||
}
|
||||
|
||||
property bool valueTodoIntegration: pluginApi?.pluginSettings?.enableTodoIntegration ?? false
|
||||
property bool valuePincardsEnabled: pluginApi?.pluginSettings?.pincardsEnabled ?? true
|
||||
property bool valueNotecardsEnabled: pluginApi?.pluginSettings?.notecardsEnabled ?? true
|
||||
property bool valueShowCloseButton: pluginApi?.pluginSettings?.showCloseButton ?? false
|
||||
property bool valueFullscreenMode: pluginApi?.pluginSettings?.fullscreenMode ?? false
|
||||
property bool valueHidePanelBackground: pluginApi?.pluginSettings?.hidePanelBackground ?? false
|
||||
property bool valueAutoPaste: pluginApi?.pluginSettings?.autoPaste ?? false
|
||||
property bool valueAutoPasteOnRightClick: pluginApi?.pluginSettings?.autoPasteOnRightClick ?? false
|
||||
property int valueAutoPasteDelay: pluginApi?.pluginSettings?.autoPasteDelay ?? 300
|
||||
property int valuePanelWidth: pluginApi?.pluginSettings?.panelWidth ?? 1450
|
||||
property int valuePanelHeight: pluginApi?.pluginSettings?.panelHeight ?? 0
|
||||
property var pendingCardColors: JSON.parse(JSON.stringify(defaultCardColors))
|
||||
property var pendingCustomColors: {
|
||||
"Text": {
|
||||
bg: "#555555",
|
||||
separator: "#000000",
|
||||
fg: "#e9e4f0"
|
||||
},
|
||||
"Image": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"Link": {
|
||||
bg: "#c7a1d8",
|
||||
separator: "#000000",
|
||||
fg: "#1a151f"
|
||||
},
|
||||
"Code": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Color": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Emoji": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"File": {
|
||||
bg: "#e9899d",
|
||||
separator: "#000000",
|
||||
fg: "#1e1418"
|
||||
}
|
||||
}
|
||||
|
||||
// ToDo integration
|
||||
property bool todoPluginAvailable: false
|
||||
property bool enableTodoIntegration: pluginApi?.pluginSettings?.enableTodoIntegration ?? false
|
||||
|
||||
// Available card types
|
||||
readonly property var cardTypes: [
|
||||
{
|
||||
key: "Text",
|
||||
name: "Text"
|
||||
},
|
||||
{
|
||||
key: "Image",
|
||||
name: "Image"
|
||||
},
|
||||
{
|
||||
key: "Link",
|
||||
name: "Link"
|
||||
},
|
||||
{
|
||||
key: "Code",
|
||||
name: "Code"
|
||||
},
|
||||
{
|
||||
key: "Color",
|
||||
name: "Color"
|
||||
},
|
||||
{
|
||||
key: "Emoji",
|
||||
name: "Emoji"
|
||||
},
|
||||
{
|
||||
key: "File",
|
||||
name: "File"
|
||||
}
|
||||
]
|
||||
|
||||
// Available colors from Color scheme
|
||||
readonly property var colorOptions: [
|
||||
{
|
||||
key: "mPrimary",
|
||||
name: "Primary"
|
||||
},
|
||||
{
|
||||
key: "mOnPrimary",
|
||||
name: "On Primary"
|
||||
},
|
||||
{
|
||||
key: "mSecondary",
|
||||
name: "Secondary"
|
||||
},
|
||||
{
|
||||
key: "mOnSecondary",
|
||||
name: "On Secondary"
|
||||
},
|
||||
{
|
||||
key: "mTertiary",
|
||||
name: "Tertiary"
|
||||
},
|
||||
{
|
||||
key: "mOnTertiary",
|
||||
name: "On Tertiary"
|
||||
},
|
||||
{
|
||||
key: "mSurface",
|
||||
name: "Surface"
|
||||
},
|
||||
{
|
||||
key: "mOnSurface",
|
||||
name: "On Surface"
|
||||
},
|
||||
{
|
||||
key: "mSurfaceVariant",
|
||||
name: "Surface Variant"
|
||||
},
|
||||
{
|
||||
key: "mOnSurfaceVariant",
|
||||
name: "On Surface Variant"
|
||||
},
|
||||
{
|
||||
key: "mOutline",
|
||||
name: "Outline"
|
||||
},
|
||||
{
|
||||
key: "mError",
|
||||
name: "Error"
|
||||
},
|
||||
{
|
||||
key: "mOnError",
|
||||
name: "On Error"
|
||||
},
|
||||
{
|
||||
key: "mHover",
|
||||
name: "Hover"
|
||||
},
|
||||
{
|
||||
key: "mOnHover",
|
||||
name: "On Hover"
|
||||
},
|
||||
{
|
||||
key: "custom",
|
||||
name: "Custom..."
|
||||
}
|
||||
]
|
||||
|
||||
// Currently selected card type for editing
|
||||
property string selectedCardType: "Text"
|
||||
|
||||
// Default colors per card type
|
||||
readonly property var defaultCardColors: {
|
||||
"Text": {
|
||||
bg: "mOutline",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSurface"
|
||||
},
|
||||
"Image": {
|
||||
bg: "mTertiary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnTertiary"
|
||||
},
|
||||
"Link": {
|
||||
bg: "mPrimary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnPrimary"
|
||||
},
|
||||
"Code": {
|
||||
bg: "mSecondary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSecondary"
|
||||
},
|
||||
"Color": {
|
||||
bg: "mSecondary",
|
||||
separator: "mSurface",
|
||||
fg: "mOnSecondary"
|
||||
},
|
||||
"Emoji": {
|
||||
bg: "mHover",
|
||||
separator: "mSurface",
|
||||
fg: "mOnHover"
|
||||
},
|
||||
"File": {
|
||||
bg: "mError",
|
||||
separator: "mSurface",
|
||||
fg: "mOnError"
|
||||
}
|
||||
}
|
||||
|
||||
// Current card colors (loaded from settings or defaults)
|
||||
property var cardColors: JSON.parse(JSON.stringify(defaultCardColors))
|
||||
|
||||
// Custom color values (when "custom" is selected)
|
||||
property var customColors: {
|
||||
"Text": {
|
||||
bg: "#555555",
|
||||
separator: "#000000",
|
||||
fg: "#e9e4f0"
|
||||
},
|
||||
"Image": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"Link": {
|
||||
bg: "#c7a1d8",
|
||||
separator: "#000000",
|
||||
fg: "#1a151f"
|
||||
},
|
||||
"Code": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Color": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Emoji": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"File": {
|
||||
bg: "#e9899d",
|
||||
separator: "#000000",
|
||||
fg: "#1e1418"
|
||||
}
|
||||
}
|
||||
|
||||
// Home directory for path resolution
|
||||
readonly property string homeDir: Quickshell.env("HOME") || ""
|
||||
|
||||
// Check if ToDo plugin is installed and enabled
|
||||
FileView {
|
||||
id: pluginsConfigFile
|
||||
path: root.homeDir + "/.config/noctalia/plugins.json"
|
||||
printErrors: false
|
||||
watchChanges: true
|
||||
onLoaded: {
|
||||
try {
|
||||
const content = text();
|
||||
if (content && content.length > 0) {
|
||||
const config = JSON.parse(content);
|
||||
root.todoPluginAvailable = config?.states?.todo?.enabled === true;
|
||||
}
|
||||
} catch (e) {
|
||||
root.todoPluginAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Snapshot settings on open for revert-on-cancel
|
||||
if (pluginApi) {
|
||||
_snapshot = JSON.parse(JSON.stringify(pluginApi.pluginSettings));
|
||||
}
|
||||
|
||||
// Load saved settings
|
||||
if (pluginApi?.pluginSettings?.enableTodoIntegration !== undefined) {
|
||||
enableTodoIntegration = pluginApi.pluginSettings.enableTodoIntegration;
|
||||
}
|
||||
if (pluginApi?.pluginSettings?.cardColors) {
|
||||
try {
|
||||
const loaded = JSON.parse(JSON.stringify(pluginApi.pluginSettings.cardColors));
|
||||
cardColors = loaded;
|
||||
pendingCardColors = JSON.parse(JSON.stringify(loaded));
|
||||
} catch (e) {
|
||||
Logger.w("Clipper", "Failed to load card colors: " + e);
|
||||
}
|
||||
}
|
||||
if (pluginApi?.pluginSettings?.customColors) {
|
||||
try {
|
||||
const loaded = JSON.parse(JSON.stringify(pluginApi.pluginSettings.customColors));
|
||||
customColors = loaded;
|
||||
pendingCustomColors = JSON.parse(JSON.stringify(loaded));
|
||||
} catch (e) {
|
||||
Logger.w("Clipper", "Failed to load custom colors: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (!_applied && pluginApi && _snapshot) {
|
||||
pluginApi.pluginSettings = Object.assign({}, _snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get actual color value
|
||||
function getColorValue(colorKey, cardType, colorType) {
|
||||
if (colorKey === "custom") {
|
||||
return customColors[cardType]?.[colorType] || "#888888";
|
||||
}
|
||||
if (typeof Color !== "undefined" && Color[colorKey]) {
|
||||
return Color[colorKey];
|
||||
}
|
||||
return "#888888";
|
||||
}
|
||||
|
||||
// Get current colors for preview
|
||||
function getPreviewBg() {
|
||||
return getColorValue(cardColors[selectedCardType]?.bg || "mOutline", selectedCardType, "bg");
|
||||
}
|
||||
function getPreviewSeparator() {
|
||||
return getColorValue(cardColors[selectedCardType]?.separator || "mSurface", selectedCardType, "separator");
|
||||
}
|
||||
function getPreviewFg() {
|
||||
return getColorValue(cardColors[selectedCardType]?.fg || "mOnSurface", selectedCardType, "fg");
|
||||
}
|
||||
|
||||
// Tab bar
|
||||
NTabBar {
|
||||
id: tabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: pluginApi?.tr("settings.tab-general")
|
||||
tabIndex: 0
|
||||
checked: tabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: pluginApi?.tr("settings.tab-appearance")
|
||||
tabIndex: 1
|
||||
checked: tabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginS
|
||||
}
|
||||
|
||||
// Tab view
|
||||
NTabView {
|
||||
id: tabView
|
||||
currentIndex: tabBar.currentIndex
|
||||
|
||||
// TAB 1: GENERAL
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
|
||||
// ===== INTEGRATIONS SECTION =====
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.integrations")
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeL
|
||||
}
|
||||
|
||||
// ToDo Integration Toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.todo-integration")
|
||||
description: root.todoPluginAvailable ? pluginApi?.tr("settings.todo-description") : pluginApi?.tr("settings.todo-disabled")
|
||||
enabled: root.todoPluginAvailable
|
||||
checked: root.valueTodoIntegration
|
||||
onToggled: checked => {
|
||||
root.valueTodoIntegration = checked;
|
||||
root._applyPreview("enableTodoIntegration", checked);
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// ===== FEATURES SECTION =====
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.features")
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeL
|
||||
}
|
||||
|
||||
// Fullscreen Mode Toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.fullscreen-mode")
|
||||
description: pluginApi?.tr("settings.fullscreen-mode-desc")
|
||||
checked: root.valueFullscreenMode
|
||||
onToggled: checked => {
|
||||
root.valueFullscreenMode = checked;
|
||||
root._applyPreview("fullscreenMode", checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel Width (hidden when fullscreen)
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.valueFullscreenMode
|
||||
label: pluginApi?.tr("settings.panel-width")
|
||||
description: pluginApi?.tr("settings.panel-width-desc")
|
||||
value: root.valuePanelWidth
|
||||
from: 400
|
||||
to: 3840
|
||||
stepSize: 50
|
||||
onValueChanged: {
|
||||
root.valuePanelWidth = value;
|
||||
root._applyPreview("panelWidth", value);
|
||||
}
|
||||
}
|
||||
|
||||
// Panel Height (hidden when fullscreen)
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.valueFullscreenMode
|
||||
label: pluginApi?.tr("settings.panel-height")
|
||||
description: pluginApi?.tr("settings.panel-height-desc")
|
||||
value: root.valuePanelHeight
|
||||
from: 0
|
||||
to: 2160
|
||||
stepSize: 50
|
||||
onValueChanged: {
|
||||
root.valuePanelHeight = value;
|
||||
root._applyPreview("panelHeight", value);
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// PinCards Enable Toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.pincards-enabled")
|
||||
description: pluginApi?.tr("settings.pincards-desc")
|
||||
checked: root.valuePincardsEnabled
|
||||
onToggled: checked => {
|
||||
root.valuePincardsEnabled = checked;
|
||||
root._applyPreview("pincardsEnabled", checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Pinned items count display
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
visible: pluginApi?.pluginSettings?.pincardsEnabled ?? true
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.pincards-items-count")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
const count = pluginApi?.mainInstance?.pinnedItems?.length || 0;
|
||||
const max = pluginApi?.mainInstance?.maxPinnedItems || 20;
|
||||
return count + " / " + max;
|
||||
}
|
||||
color: {
|
||||
const count = pluginApi?.mainInstance?.pinnedItems?.length || 0;
|
||||
const max = pluginApi?.mainInstance?.maxPinnedItems || 20;
|
||||
return count >= max ? Color.mError : Color.mOnSurface;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all pinned items button
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: pluginApi?.tr("settings.clear-all-pinned")
|
||||
icon: "trash"
|
||||
visible: pluginApi?.pluginSettings?.pincardsEnabled ?? true
|
||||
enabled: (pluginApi?.mainInstance?.pinnedItems?.length || 0) > 0
|
||||
onClicked: {
|
||||
if (pluginApi?.mainInstance) {
|
||||
// Clear all pinned items
|
||||
pluginApi.mainInstance.pinnedItems = [];
|
||||
pluginApi.mainInstance.savePinnedFile();
|
||||
pluginApi.mainInstance.pinnedRevision++;
|
||||
ToastService.showNotice(pluginApi?.tr("toast.pinned-cleared"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NoteCards Enable Toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.notecards-enabled")
|
||||
description: pluginApi?.tr("settings.notecards-desc")
|
||||
checked: root.valueNotecardsEnabled
|
||||
onToggled: checked => {
|
||||
root.valueNotecardsEnabled = checked;
|
||||
root._applyPreview("notecardsEnabled", checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Notes count display
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
visible: pluginApi?.pluginSettings?.notecardsEnabled ?? true
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.notecards-notes-count")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: {
|
||||
const count = pluginApi?.mainInstance?.noteCards?.length || 0;
|
||||
const max = pluginApi?.mainInstance?.maxNoteCards || 20;
|
||||
return count + " / " + max;
|
||||
}
|
||||
color: {
|
||||
const count = pluginApi?.mainInstance?.noteCards?.length || 0;
|
||||
const max = pluginApi?.mainInstance?.maxNoteCards || 20;
|
||||
return count >= max ? Color.mError : Color.mOnSurface;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all notes button
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: pluginApi?.tr("settings.clear-all-notes")
|
||||
icon: "trash"
|
||||
visible: pluginApi?.pluginSettings?.notecardsEnabled ?? true
|
||||
enabled: (pluginApi?.mainInstance?.noteCards?.length || 0) > 0
|
||||
onClicked: {
|
||||
if (pluginApi?.mainInstance) {
|
||||
pluginApi.mainInstance.clearAllNoteCards();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Hide Panel Background Toggle (hidden when Notecards enabled — no effect with notecards)
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.valueNotecardsEnabled
|
||||
label: pluginApi?.tr("settings.hide-panel-background")
|
||||
description: pluginApi?.tr("settings.hide-panel-background-desc")
|
||||
checked: root.valueHidePanelBackground
|
||||
onToggled: checked => {
|
||||
root.valueHidePanelBackground = checked;
|
||||
root._applyPreview("hidePanelBackground", checked);
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: !root.valueNotecardsEnabled
|
||||
}
|
||||
|
||||
// ===== AUTO-PASTE SECTION =====
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.auto-paste-section")
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeL
|
||||
}
|
||||
|
||||
// Auto-Paste Toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.auto-paste")
|
||||
description: pluginApi?.tr("settings.auto-paste-desc")
|
||||
checked: root.valueAutoPaste
|
||||
onToggled: checked => {
|
||||
root.valueAutoPaste = checked;
|
||||
root._applyPreview("autoPaste", checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Warning: wtype not installed (visible only when autoPaste=true and wtype unavailable)
|
||||
Rectangle {
|
||||
visible: root.valueAutoPaste && !(pluginApi?.mainInstance?.wtypeAvailable ?? false)
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: warningText.implicitHeight + Style.marginM * 2
|
||||
color: (typeof Color !== "undefined") ? Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.15) : "#33CC0000"
|
||||
radius: Style.radiusS
|
||||
border.width: 1
|
||||
border.color: (typeof Color !== "undefined") ? Color.mError : "#CC0000"
|
||||
|
||||
NText {
|
||||
id: warningText
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
text: pluginApi?.tr("settings.auto-paste-warning")
|
||||
wrapMode: Text.Wrap
|
||||
color: (typeof Color !== "undefined") ? Color.mError : "#CC0000"
|
||||
font.pointSize: Style.fontSizeS
|
||||
}
|
||||
}
|
||||
|
||||
// RMB Only Toggle (visible only when autoPaste=true)
|
||||
NToggle {
|
||||
visible: root.valueAutoPaste
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.auto-paste-rmb")
|
||||
description: pluginApi?.tr("settings.auto-paste-rmb-desc")
|
||||
checked: root.valueAutoPasteOnRightClick
|
||||
onToggled: checked => {
|
||||
root.valueAutoPasteOnRightClick = checked;
|
||||
root._applyPreview("autoPasteOnRightClick", checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Paste Delay Row (visible only when autoPaste=true)
|
||||
ColumnLayout {
|
||||
visible: root.valueAutoPaste
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.auto-paste-delay")
|
||||
description: pluginApi?.tr("settings.auto-paste-delay-desc")
|
||||
from: 100
|
||||
to: 1000
|
||||
stepSize: 50
|
||||
value: root.valueAutoPasteDelay
|
||||
text: root.valueAutoPasteDelay + " ms"
|
||||
onMoved: value => {
|
||||
root.valueAutoPasteDelay = Math.round(value);
|
||||
root._applyPreview("autoPasteDelay", Math.round(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Show close button toggle
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.show-close-button")
|
||||
description: pluginApi?.tr("settings.show-close-button-desc")
|
||||
checked: root.valueShowCloseButton
|
||||
onToggled: checked => {
|
||||
root.valueShowCloseButton = checked;
|
||||
root._applyPreview("showCloseButton", checked);
|
||||
}
|
||||
}
|
||||
} // End General Tab
|
||||
|
||||
// TAB 2: APPEARANCE
|
||||
ColumnLayout {
|
||||
spacing: Style.marginL
|
||||
|
||||
// ===== APPEARANCE SECTION =====
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.appearance")
|
||||
font.bold: true
|
||||
font.pointSize: Style.fontSizeL
|
||||
}
|
||||
|
||||
// Card type selector
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.card-type")
|
||||
description: pluginApi?.tr("settings.card-type-desc")
|
||||
model: root.cardTypes
|
||||
currentKey: root.selectedCardType
|
||||
onSelected: key => root.selectedCardType = key
|
||||
}
|
||||
|
||||
// Live Preview
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 280
|
||||
color: (typeof Color !== "undefined") ? Color.mSurfaceVariant : "#333333"
|
||||
radius: Style.radiusM
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.preview")
|
||||
font.bold: true
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
|
||||
// Preview card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 250
|
||||
Layout.preferredHeight: 220
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: root.getPreviewBg()
|
||||
radius: Style.radiusM
|
||||
border.width: 2
|
||||
border.color: root.getPreviewBg()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
color: root.getPreviewBg()
|
||||
radius: Style.radiusM
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: parent.radius
|
||||
color: parent.color
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
NIcon {
|
||||
icon: root.selectedCardType === "Image" ? "photo" : root.selectedCardType === "Link" ? "link" : root.selectedCardType === "Code" ? "code" : root.selectedCardType === "Color" ? "palette" : root.selectedCardType === "Emoji" ? "mood-smile" : root.selectedCardType === "File" ? "file" : "align-left"
|
||||
pointSize: 12
|
||||
color: root.getPreviewFg()
|
||||
}
|
||||
|
||||
NText {
|
||||
text: root.selectedCardType
|
||||
font.bold: true
|
||||
color: root.getPreviewFg()
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "trash"
|
||||
pointSize: 12
|
||||
color: root.getPreviewFg()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
Layout.preferredWidth: parent.width - 10
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredHeight: 1
|
||||
color: root.getPreviewSeparator()
|
||||
}
|
||||
|
||||
// Content area
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 8
|
||||
|
||||
NText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
text: pluginApi?.tr("settings.sample-content")
|
||||
wrapMode: Text.Wrap
|
||||
color: root.getPreviewFg()
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color settings
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.bg-color")
|
||||
description: pluginApi?.tr("settings.bg-color-desc")
|
||||
model: root.colorOptions
|
||||
currentKey: root.cardColors[root.selectedCardType]?.bg || "mOutline"
|
||||
onSelected: key => {
|
||||
if (!root.pendingCardColors[root.selectedCardType])
|
||||
root.pendingCardColors[root.selectedCardType] = {};
|
||||
root.pendingCardColors[root.selectedCardType].bg = key;
|
||||
root.cardColors = JSON.parse(JSON.stringify(root.pendingCardColors));
|
||||
}
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
visible: root.cardColors[root.selectedCardType]?.bg === "custom"
|
||||
Layout.preferredWidth: Style.sliderWidth
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
selectedColor: root.customColors[root.selectedCardType]?.bg || "#888888"
|
||||
onColorSelected: color => {
|
||||
if (!root.pendingCustomColors[root.selectedCardType])
|
||||
root.pendingCustomColors[root.selectedCardType] = {};
|
||||
root.pendingCustomColors[root.selectedCardType].bg = color.toString();
|
||||
root.customColors = JSON.parse(JSON.stringify(root.pendingCustomColors));
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.separator-color")
|
||||
description: pluginApi?.tr("settings.separator-color-desc")
|
||||
model: root.colorOptions
|
||||
currentKey: root.cardColors[root.selectedCardType]?.separator || "mSurface"
|
||||
onSelected: key => {
|
||||
if (!root.pendingCardColors[root.selectedCardType])
|
||||
root.pendingCardColors[root.selectedCardType] = {};
|
||||
root.pendingCardColors[root.selectedCardType].separator = key;
|
||||
root.cardColors = JSON.parse(JSON.stringify(root.pendingCardColors));
|
||||
}
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
visible: root.cardColors[root.selectedCardType]?.separator === "custom"
|
||||
Layout.preferredWidth: Style.sliderWidth
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
selectedColor: root.customColors[root.selectedCardType]?.separator || "#000000"
|
||||
onColorSelected: color => {
|
||||
if (!root.pendingCustomColors[root.selectedCardType])
|
||||
root.pendingCustomColors[root.selectedCardType] = {};
|
||||
root.pendingCustomColors[root.selectedCardType].separator = color.toString();
|
||||
root.customColors = JSON.parse(JSON.stringify(root.pendingCustomColors));
|
||||
}
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.fg-color")
|
||||
description: pluginApi?.tr("settings.fg-color-desc")
|
||||
model: root.colorOptions
|
||||
currentKey: root.cardColors[root.selectedCardType]?.fg || "mOnSurface"
|
||||
onSelected: key => {
|
||||
if (!root.pendingCardColors[root.selectedCardType])
|
||||
root.pendingCardColors[root.selectedCardType] = {};
|
||||
root.pendingCardColors[root.selectedCardType].fg = key;
|
||||
root.cardColors = JSON.parse(JSON.stringify(root.pendingCardColors));
|
||||
}
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
visible: root.cardColors[root.selectedCardType]?.fg === "custom"
|
||||
Layout.preferredWidth: Style.sliderWidth
|
||||
Layout.preferredHeight: Style.baseWidgetSize
|
||||
selectedColor: root.customColors[root.selectedCardType]?.fg || "#e9e4f0"
|
||||
onColorSelected: color => {
|
||||
if (!root.pendingCustomColors[root.selectedCardType])
|
||||
root.pendingCustomColors[root.selectedCardType] = {};
|
||||
root.pendingCustomColors[root.selectedCardType].fg = color.toString();
|
||||
root.customColors = JSON.parse(JSON.stringify(root.pendingCustomColors));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset button
|
||||
NButton {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: pluginApi?.tr("settings.reset-defaults")
|
||||
icon: "refresh"
|
||||
onClicked: {
|
||||
const defaults = JSON.parse(JSON.stringify(root.defaultCardColors));
|
||||
const defaultCustom = {
|
||||
"Text": {
|
||||
bg: "#555555",
|
||||
separator: "#000000",
|
||||
fg: "#e9e4f0"
|
||||
},
|
||||
"Image": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"Link": {
|
||||
bg: "#c7a1d8",
|
||||
separator: "#000000",
|
||||
fg: "#1a151f"
|
||||
},
|
||||
"Code": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Color": {
|
||||
bg: "#a984c4",
|
||||
separator: "#000000",
|
||||
fg: "#f3edf7"
|
||||
},
|
||||
"Emoji": {
|
||||
bg: "#e0b7c9",
|
||||
separator: "#000000",
|
||||
fg: "#20161f"
|
||||
},
|
||||
"File": {
|
||||
bg: "#e9899d",
|
||||
separator: "#000000",
|
||||
fg: "#1e1418"
|
||||
}
|
||||
};
|
||||
root.pendingCardColors = defaults;
|
||||
root.pendingCustomColors = defaultCustom;
|
||||
root.cardColors = JSON.parse(JSON.stringify(defaults));
|
||||
root.customColors = JSON.parse(JSON.stringify(defaultCustom));
|
||||
}
|
||||
}
|
||||
} // End Appearance Tab
|
||||
|
||||
} // End NTabView
|
||||
|
||||
function saveSettings() {
|
||||
if (!pluginApi)
|
||||
return;
|
||||
|
||||
// Belt-and-suspenders: guarantee final state is correct even if a _applyPreview was missed.
|
||||
pluginApi.pluginSettings.enableTodoIntegration = root.valueTodoIntegration;
|
||||
pluginApi.pluginSettings.pincardsEnabled = root.valuePincardsEnabled;
|
||||
pluginApi.pluginSettings.notecardsEnabled = root.valueNotecardsEnabled;
|
||||
pluginApi.pluginSettings.showCloseButton = root.valueShowCloseButton;
|
||||
pluginApi.pluginSettings.fullscreenMode = root.valueFullscreenMode;
|
||||
pluginApi.pluginSettings.hidePanelBackground = root.valueHidePanelBackground;
|
||||
pluginApi.pluginSettings.autoPaste = root.valueAutoPaste;
|
||||
pluginApi.pluginSettings.autoPasteOnRightClick = root.valueAutoPasteOnRightClick;
|
||||
pluginApi.pluginSettings.autoPasteDelay = root.valueAutoPasteDelay;
|
||||
pluginApi.pluginSettings.panelWidth = root.valuePanelWidth;
|
||||
pluginApi.pluginSettings.panelHeight = root.valuePanelHeight;
|
||||
pluginApi.pluginSettings.cardColors = JSON.parse(JSON.stringify(root.pendingCardColors));
|
||||
pluginApi.pluginSettings.customColors = JSON.parse(JSON.stringify(root.pendingCustomColors));
|
||||
|
||||
if (pluginApi.mainInstance) {
|
||||
pluginApi.mainInstance.showCloseButton = root.valueShowCloseButton;
|
||||
}
|
||||
|
||||
_applied = true;
|
||||
pluginApi.saveSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
// Todo page selector wrapper - prepares menu items and handles selection
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property string selectedText: ""
|
||||
property var todoPages: []
|
||||
property var selectionMenu: null
|
||||
|
||||
signal pageSelected(int pageId, string pageName)
|
||||
|
||||
function buildMenuModel() {
|
||||
const model = [];
|
||||
|
||||
// Add existing todo pages only
|
||||
for (let i = 0; i < todoPages.length; i++) {
|
||||
const page = todoPages[i];
|
||||
model.push({
|
||||
"label": page.name || pluginApi?.tr("notecards.untitled-placeholder"),
|
||||
"action": "page-" + page.id,
|
||||
"icon": "list",
|
||||
"pageId": page.id
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
function show(text, pages) {
|
||||
selectedText = text || "";
|
||||
todoPages = pages || [];
|
||||
|
||||
if (selectionMenu) {
|
||||
const menuItems = buildMenuModel();
|
||||
selectionMenu.show(menuItems);
|
||||
} else {}
|
||||
}
|
||||
|
||||
function handleItemSelected(action) {
|
||||
if (action.startsWith("page-")) {
|
||||
const pageId = parseInt(action.replace("page-", ""));
|
||||
const page = todoPages.find(p => p.id === pageId);
|
||||
root.pageSelected(pageId, page ? page.name : pluginApi?.tr("notecards.untitled-placeholder"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Zwischenablage-Verlauf"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipper umschalten",
|
||||
"settings": "Einstellungen öffnen"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Zwischenablage-Verlauf",
|
||||
"pinned-title": "Angeheftete Elemente",
|
||||
"no-pinned": "Keine angehefteten Elemente",
|
||||
"settings": "Einstellungen",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Suchen...",
|
||||
"filter-all": "Alle",
|
||||
"filter-text": "Text",
|
||||
"filter-images": "Bilder",
|
||||
"filter-colors": "Farben",
|
||||
"filter-links": "Links",
|
||||
"filter-code": "Code",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Dateien",
|
||||
"clear-all": "Alles löschen",
|
||||
"no-matches": "Keine übereinstimmenden Elemente",
|
||||
"empty": "Zwischenablage ist leer"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Zur Aufgabenliste hinzufügen",
|
||||
"pin": "Anheften",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrationen",
|
||||
"todo-integration": "ToDo-Plugin-Integration",
|
||||
"todo-description": "Zwischenablage-Elemente direkt zu Ihrer Aufgabenliste hinzufügen",
|
||||
"todo-disabled": "ToDo-Plugin ist nicht installiert oder deaktiviert",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Vollbildmodus",
|
||||
"fullscreen-mode-desc": "Das Clipboard-Panel auf den gesamten Bildschirm erweitern",
|
||||
"hide-panel-background": "Panelhintergrund ausblenden",
|
||||
"hide-panel-background-desc": "Hintergrund der Panelcontainer entfernen. Hinweis: Pin Cards und Note Cards funktionieren am besten mit aktiviertem Hintergrund.",
|
||||
"auto-paste-section": "Automatisches Einfügen",
|
||||
"auto-paste": "Beim Klick automatisch einfügen",
|
||||
"auto-paste-desc": "Clipboard-Inhalt nach der Auswahl automatisch in das fokussierte Fenster einfügen",
|
||||
"auto-paste-warning": "wtype wird für das automatische Einfügen benötigt. Installieren mit: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Nur Rechtsklick",
|
||||
"auto-paste-rmb-desc": "Rechtsklick zum automatischen Einfügen; Linksklick kopiert nur in die Zwischenablage",
|
||||
"auto-paste-delay": "Einfügeverzögerung",
|
||||
"auto-paste-delay-desc": "Verzögerung vor dem Einfügen (ms). Erhöhen wenn Fokus-folgt-Maus in Ihrem Compositor aktiv ist.",
|
||||
"appearance": "Kartendarstellung",
|
||||
"card-type": "Kartentyp",
|
||||
"card-type-desc": "Wählen Sie den Kartentyp zum Anpassen",
|
||||
"preview": "Vorschau",
|
||||
"sample-content": "Beispielinhalt...",
|
||||
"bg-color": "Hintergrundfarbe",
|
||||
"bg-color-desc": "Hintergrundfarbe der Karte",
|
||||
"separator-color": "Trennlinienfarbe",
|
||||
"separator-color-desc": "Linie zwischen Kopfzeile und Inhalt",
|
||||
"fg-color": "Vordergrundfarbe",
|
||||
"fg-color-desc": "Farbe für Titel, Symbole und Textinhalt",
|
||||
"reset-defaults": "Auf Standardwerte zurücksetzen",
|
||||
"panel-width": "Panelbreite",
|
||||
"panel-width-desc": "Breite des Panels in Pixeln (nur wenn der Vollbildmodus deaktiviert ist)",
|
||||
"panel-height": "Panelhöhe",
|
||||
"panel-height-desc": "Höhe des Panels in Pixeln. Auf 0 setzen für automatische Höhe (nur wenn der Vollbildmodus deaktiviert ist)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Clipboard History"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Toggle Clipper",
|
||||
"settings": "Open Settings"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Clipboard History",
|
||||
"pinned-title": "Pinned Items",
|
||||
"no-pinned": "No pinned items",
|
||||
"settings": "Settings",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Search...",
|
||||
"filter-all": "All",
|
||||
"filter-text": "Text",
|
||||
"filter-images": "Images",
|
||||
"filter-colors": "Colors",
|
||||
"filter-links": "Links",
|
||||
"filter-code": "Code",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Files",
|
||||
"clear-all": "Clear All",
|
||||
"no-matches": "No matching items",
|
||||
"empty": "Clipboard is empty"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Add to ToDo",
|
||||
"pin": "Pin",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrations",
|
||||
"todo-integration": "ToDo Plugin Integration",
|
||||
"todo-description": "Add clipboard items directly to your ToDo list",
|
||||
"todo-disabled": "ToDo plugin is not installed or disabled",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes and sticky notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Card Appearance",
|
||||
"card-type": "Card Type",
|
||||
"card-type-desc": "Select card type to customize",
|
||||
"preview": "Preview",
|
||||
"sample-content": "Sample content preview...",
|
||||
"bg-color": "Background Color",
|
||||
"bg-color-desc": "Card background color",
|
||||
"separator-color": "Separator Color",
|
||||
"separator-color-desc": "Line between header and content",
|
||||
"fg-color": "Foreground Color",
|
||||
"fg-color-desc": "Title, icons and content text color",
|
||||
"reset-defaults": "Reset to Defaults",
|
||||
"panel-width": "Panel Width",
|
||||
"panel-width-desc": "Width of the panel in pixels (only when fullscreen mode is disabled)",
|
||||
"panel-height": "Panel Height",
|
||||
"panel-height-desc": "Height of the panel in pixels. Set to 0 for automatic height (only when fullscreen mode is disabled)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Historial del portapapeles"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Alternar Clipper",
|
||||
"settings": "Abrir configuración"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Historial del portapapeles",
|
||||
"pinned-title": "Elementos fijados",
|
||||
"no-pinned": "Sin elementos fijados",
|
||||
"settings": "Configuración",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Buscar...",
|
||||
"filter-all": "Todos",
|
||||
"filter-text": "Texto",
|
||||
"filter-images": "Imágenes",
|
||||
"filter-colors": "Colores",
|
||||
"filter-links": "Enlaces",
|
||||
"filter-code": "Código",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Archivos",
|
||||
"clear-all": "Borrar todo",
|
||||
"no-matches": "Sin coincidencias",
|
||||
"empty": "El portapapeles está vacío"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Añadir a tareas",
|
||||
"pin": "Fijar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integraciones",
|
||||
"todo-integration": "Integración con ToDo",
|
||||
"todo-description": "Añadir elementos del portapapeles directamente a tu lista de tareas",
|
||||
"todo-disabled": "El plugin ToDo no está instalado o está deshabilitado",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Modo pantalla completa",
|
||||
"fullscreen-mode-desc": "Expandir el panel al tamaño completo de la pantalla",
|
||||
"hide-panel-background": "Ocultar fondo del panel",
|
||||
"hide-panel-background-desc": "Eliminar el fondo de los contenedores del panel. Nota: Las tarjetas ancladas y las notas funcionan mejor con el fondo activado.",
|
||||
"auto-paste-section": "Pegado automático",
|
||||
"auto-paste": "Pegar automáticamente al hacer clic",
|
||||
"auto-paste-desc": "Pegar automáticamente el elemento seleccionado en la ventana activa",
|
||||
"auto-paste-warning": "wtype es necesario para el pegado automático. Instalar con: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Solo clic derecho",
|
||||
"auto-paste-rmb-desc": "Clic derecho para pegar automáticamente; clic izquierdo solo copia al portapapeles",
|
||||
"auto-paste-delay": "Retraso de pegado",
|
||||
"auto-paste-delay-desc": "Retraso antes de pegar (ms). Aumentar si focus-follows-cursor está activado en tu compositor.",
|
||||
"appearance": "Apariencia de tarjetas",
|
||||
"card-type": "Tipo de tarjeta",
|
||||
"card-type-desc": "Selecciona el tipo de tarjeta para personalizar",
|
||||
"preview": "Vista previa",
|
||||
"sample-content": "Contenido de ejemplo...",
|
||||
"bg-color": "Color de fondo",
|
||||
"bg-color-desc": "Color de fondo de la tarjeta",
|
||||
"separator-color": "Color del separador",
|
||||
"separator-color-desc": "Línea entre el encabezado y el contenido",
|
||||
"fg-color": "Color del texto",
|
||||
"fg-color-desc": "Color del título, iconos y contenido de texto",
|
||||
"reset-defaults": "Restaurar valores predeterminados",
|
||||
"panel-width": "Ancho del panel",
|
||||
"panel-width-desc": "Ancho del panel en píxeles (solo cuando el modo pantalla completa está desactivado)",
|
||||
"panel-height": "Alto del panel",
|
||||
"panel-height-desc": "Alto del panel en píxeles. Establecer en 0 para altura automática (solo cuando el modo pantalla completa está desactivado)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Historique du presse-papiers"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Basculer Clipper",
|
||||
"settings": "Ouvrir les paramètres"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Historique du presse-papiers",
|
||||
"pinned-title": "Éléments épinglés",
|
||||
"no-pinned": "Aucun élément épinglé",
|
||||
"settings": "Paramètres",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Rechercher...",
|
||||
"filter-all": "Tous",
|
||||
"filter-text": "Texte",
|
||||
"filter-images": "Images",
|
||||
"filter-colors": "Couleurs",
|
||||
"filter-links": "Liens",
|
||||
"filter-code": "Code",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Fichiers",
|
||||
"clear-all": "Tout effacer",
|
||||
"no-matches": "Aucun élément correspondant",
|
||||
"empty": "Le presse-papiers est vide"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Ajouter aux tâches",
|
||||
"pin": "Épingler",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Intégrations",
|
||||
"todo-integration": "Intégration du plugin ToDo",
|
||||
"todo-description": "Ajouter des éléments du presse-papiers directement à votre liste de tâches",
|
||||
"todo-disabled": "Le plugin ToDo n'est pas installé ou est désactivé",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Mode plein écran",
|
||||
"fullscreen-mode-desc": "Étendre le panneau au plein écran",
|
||||
"hide-panel-background": "Masquer l'arrière-plan",
|
||||
"hide-panel-background-desc": "Supprimer l'arrière-plan des conteneurs du panneau. Note: Les cartes épinglées et les notes fonctionnent mieux avec l'arrière-plan activé.",
|
||||
"auto-paste-section": "Collage automatique",
|
||||
"auto-paste": "Coller automatiquement au clic",
|
||||
"auto-paste-desc": "Coller automatiquement l'élément sélectionné dans la fenêtre active",
|
||||
"auto-paste-warning": "wtype est requis pour le collage automatique. Installer avec: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Clic droit uniquement",
|
||||
"auto-paste-rmb-desc": "Clic droit pour coller automatiquement; clic gauche copie dans le presse-papiers",
|
||||
"auto-paste-delay": "Délai de collage",
|
||||
"auto-paste-delay-desc": "Délai avant le collage (ms). Augmenter si focus-suit-curseur est activé dans votre compositeur.",
|
||||
"appearance": "Apparence des cartes",
|
||||
"card-type": "Type de carte",
|
||||
"card-type-desc": "Sélectionner le type de carte à personnaliser",
|
||||
"preview": "Aperçu",
|
||||
"sample-content": "Aperçu du contenu...",
|
||||
"bg-color": "Couleur d'arrière-plan",
|
||||
"bg-color-desc": "Couleur d'arrière-plan de la carte",
|
||||
"separator-color": "Couleur du séparateur",
|
||||
"separator-color-desc": "Ligne entre l'en-tête et le contenu",
|
||||
"fg-color": "Couleur de premier plan",
|
||||
"fg-color-desc": "Couleur du titre, des icônes et du texte",
|
||||
"reset-defaults": "Réinitialiser les valeurs par défaut",
|
||||
"panel-width": "Largeur du panneau",
|
||||
"panel-width-desc": "Largeur du panneau en pixels (uniquement quand le mode plein écran est désactivé)",
|
||||
"panel-height": "Hauteur du panneau",
|
||||
"panel-height-desc": "Hauteur du panneau en pixels. Mettre à 0 pour hauteur automatique (uniquement quand le mode plein écran est désactivé)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Vágólap előzmények"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipper váltása",
|
||||
"settings": "Beállítások megnyitása"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Vágólap előzmények",
|
||||
"pinned-title": "Rögzített elemek",
|
||||
"no-pinned": "Nincs rögzített elem",
|
||||
"settings": "Beállítások",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Keresés...",
|
||||
"filter-all": "Minden",
|
||||
"filter-text": "Szöveg",
|
||||
"filter-images": "Képek",
|
||||
"filter-colors": "Színek",
|
||||
"filter-links": "Hivatkozások",
|
||||
"filter-code": "Kód",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Fájlok",
|
||||
"clear-all": "Összes törlése",
|
||||
"no-matches": "Nincs egyező elem",
|
||||
"empty": "A vágólap üres"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Hozzáadás a feladatokhoz",
|
||||
"pin": "Rögzítés",
|
||||
"delete": "Törlés"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrációk",
|
||||
"todo-integration": "ToDo beépülő integráció",
|
||||
"todo-description": "Vágólap elemek hozzáadása közvetlenül a feladatlistához",
|
||||
"todo-disabled": "A ToDo beépülő nincs telepítve vagy le van tiltva",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Kártya megjelenése",
|
||||
"card-type": "Kártya típusa",
|
||||
"card-type-desc": "Válassza ki a testreszabandó kártya típust",
|
||||
"preview": "Előnézet",
|
||||
"sample-content": "Minta tartalom előnézete...",
|
||||
"bg-color": "Háttérszín",
|
||||
"bg-color-desc": "Kártya háttérszíne",
|
||||
"separator-color": "Elválasztó színe",
|
||||
"separator-color-desc": "Vonal a fejléc és a tartalom között",
|
||||
"fg-color": "Előtérszín",
|
||||
"fg-color-desc": "Cím, ikonok és szöveges tartalom színe",
|
||||
"reset-defaults": "Alapértelmezések visszaállítása",
|
||||
"panel-width": "Panel szélessége",
|
||||
"panel-width-desc": "A panel szélessége képpontokban (csak ha a teljes képernyős mód ki van kapcsolva)",
|
||||
"panel-height": "Panel magassága",
|
||||
"panel-height-desc": "A panel magassága képpontokban. 0 értéknél automatikus magasság (csak ha a teljes képernyős mód ki van kapcsolva)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Cronologia appunti"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Attiva/disattiva Clipper",
|
||||
"settings": "Apri impostazioni"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Cronologia appunti",
|
||||
"pinned-title": "Elementi bloccati",
|
||||
"no-pinned": "Nessun elemento bloccato",
|
||||
"settings": "Impostazioni",
|
||||
"close": "Chiudi",
|
||||
"search-placeholder": "Cerca...",
|
||||
"filter-all": "Tutti",
|
||||
"filter-text": "Testo",
|
||||
"filter-images": "Immagini",
|
||||
"filter-colors": "Colori",
|
||||
"filter-links": "Link",
|
||||
"filter-code": "Codice",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "File",
|
||||
"clear-all": "Cancella tutto",
|
||||
"no-matches": "Nessun elemento corrispondente",
|
||||
"empty": "Gli appunti sono vuoti"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Aggiungi a ToDo",
|
||||
"pin": "Blocca",
|
||||
"delete": "Elimina"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrazioni",
|
||||
"todo-integration": "Integrazione plugin ToDo",
|
||||
"todo-description": "Aggiungi elementi dagli appunti direttamente alla lista ToDo",
|
||||
"todo-disabled": "Plugin ToDo non installato o disabilitato",
|
||||
"features": "Funzionalità",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes and sticky notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Modalità schermo intero",
|
||||
"fullscreen-mode-desc": "Espandi il pannello degli appunti a tutto schermo",
|
||||
"hide-panel-background": "Nascondi sfondo pannello",
|
||||
"hide-panel-background-desc": "Rimuovi lo sfondo dai contenitori del pannello. Nota: Le card bloccate e le note funzionano meglio con lo sfondo attivo.",
|
||||
"auto-paste-section": "Incolla automaticamente",
|
||||
"auto-paste": "Incolla automaticamente al clic",
|
||||
"auto-paste-desc": "Incolla automaticamente l'elemento selezionato nella finestra attiva",
|
||||
"auto-paste-warning": "wtype è necessario per l'incolla automatico. Installa con: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Solo clic destro",
|
||||
"auto-paste-rmb-desc": "Clic destro per incollare automaticamente; clic sinistro copia solo negli appunti",
|
||||
"auto-paste-delay": "Ritardo incolla",
|
||||
"auto-paste-delay-desc": "Ritardo prima di incollare (ms). Aumenta se il focus-segue-cursore è attivo nel tuo compositore.",
|
||||
"appearance": "Aspetto card",
|
||||
"card-type": "Tipo card",
|
||||
"card-type-desc": "Seleziona il tipo di card da personalizzare",
|
||||
"preview": "Anteprima",
|
||||
"sample-content": "Anteprima contenuto di esempio...",
|
||||
"bg-color": "Colore sfondo",
|
||||
"bg-color-desc": "Colore di sfondo della card",
|
||||
"separator-color": "Colore separatore",
|
||||
"separator-color-desc": "Linea tra intestazione e contenuto",
|
||||
"fg-color": "Colore primo piano",
|
||||
"fg-color-desc": "Colore per titolo, icone e testo",
|
||||
"reset-defaults": "Ripristina impostazioni predefinite",
|
||||
"panel-width": "Larghezza del pannello",
|
||||
"panel-width-desc": "Larghezza del pannello in pixel (solo quando la modalità schermo intero è disattivata)",
|
||||
"panel-height": "Altezza del pannello",
|
||||
"panel-height-desc": "Altezza del pannello in pixel. Imposta 0 per altezza automatica (solo quando la modalità schermo intero è disattivata)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "クリップボード履歴"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipperを切り替え",
|
||||
"settings": "設定を開く"
|
||||
},
|
||||
"panel": {
|
||||
"title": "クリップボード履歴",
|
||||
"pinned-title": "ピン留めされたアイテム",
|
||||
"no-pinned": "ピン留めされたアイテムはありません",
|
||||
"settings": "設定",
|
||||
"close": "Close",
|
||||
"search-placeholder": "検索...",
|
||||
"filter-all": "すべて",
|
||||
"filter-text": "テキスト",
|
||||
"filter-images": "画像",
|
||||
"filter-colors": "色",
|
||||
"filter-links": "リンク",
|
||||
"filter-code": "コード",
|
||||
"filter-emoji": "絵文字",
|
||||
"filter-files": "ファイル",
|
||||
"clear-all": "すべてクリア",
|
||||
"no-matches": "一致するアイテムがありません",
|
||||
"empty": "クリップボードは空です"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "ToDoに追加",
|
||||
"pin": "ピン留め",
|
||||
"delete": "削除"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "統合",
|
||||
"todo-integration": "ToDoプラグイン統合",
|
||||
"todo-description": "クリップボードアイテムをToDoリストに直接追加",
|
||||
"todo-disabled": "ToDoプラグインがインストールされていないか無効です",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "カードの外観",
|
||||
"card-type": "カードタイプ",
|
||||
"card-type-desc": "カスタマイズするカードタイプを選択",
|
||||
"preview": "プレビュー",
|
||||
"sample-content": "サンプルコンテンツのプレビュー...",
|
||||
"bg-color": "背景色",
|
||||
"bg-color-desc": "カードの背景色",
|
||||
"separator-color": "区切り線の色",
|
||||
"separator-color-desc": "ヘッダーとコンテンツ間の線",
|
||||
"fg-color": "前景色",
|
||||
"fg-color-desc": "タイトル、アイコン、テキストコンテンツの色",
|
||||
"reset-defaults": "デフォルトにリセット",
|
||||
"panel-width": "パネルの幅",
|
||||
"panel-width-desc": "パネルの幅をピクセル単位で指定(全画面モードが無効の場合のみ)",
|
||||
"panel-height": "パネルの高さ",
|
||||
"panel-height-desc": "パネルの高さをピクセル単位で指定。0 で自動高さ(全画面モードが無効の場合のみ)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "클립보드 기록"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipper 전환",
|
||||
"settings": "설정 열기"
|
||||
},
|
||||
"panel": {
|
||||
"title": "클립보드 기록",
|
||||
"pinned-title": "고정된 항목",
|
||||
"no-pinned": "고정된 항목이 없습니다",
|
||||
"settings": "설정",
|
||||
"close": "Close",
|
||||
"search-placeholder": "검색...",
|
||||
"filter-all": "전체",
|
||||
"filter-text": "텍스트",
|
||||
"filter-images": "이미지",
|
||||
"filter-colors": "색상",
|
||||
"filter-links": "링크",
|
||||
"filter-code": "코드",
|
||||
"filter-emoji": "이모지",
|
||||
"filter-files": "파일",
|
||||
"clear-all": "모두 지우기",
|
||||
"no-matches": "일치하는 항목이 없습니다",
|
||||
"empty": "클립보드가 비어 있습니다"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "할 일에 추가",
|
||||
"pin": "고정",
|
||||
"delete": "삭제"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "통합",
|
||||
"todo-integration": "ToDo 플러그인 통합",
|
||||
"todo-description": "클립보드 항목을 할 일 목록에 직접 추가",
|
||||
"todo-disabled": "ToDo 플러그인이 설치되지 않았거나 비활성화됨",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "카드 모양",
|
||||
"card-type": "카드 유형",
|
||||
"card-type-desc": "사용자 지정할 카드 유형 선택",
|
||||
"preview": "미리보기",
|
||||
"sample-content": "샘플 콘텐츠 미리보기...",
|
||||
"bg-color": "배경색",
|
||||
"bg-color-desc": "카드 배경색",
|
||||
"separator-color": "구분선 색상",
|
||||
"separator-color-desc": "헤더와 콘텐츠 사이의 선",
|
||||
"fg-color": "전경색",
|
||||
"fg-color-desc": "제목, 아이콘 및 텍스트 콘텐츠 색상",
|
||||
"reset-defaults": "기본값으로 재설정",
|
||||
"panel-width": "패널 너비",
|
||||
"panel-width-desc": "패널의 너비(픽셀 단위, 전체 화면 모드가 비활성화된 경우에만 적용)",
|
||||
"panel-height": "패널 높이",
|
||||
"panel-height-desc": "패널의 높이(픽셀 단위). 0으로 설정하면 자동 높이(전체 화면 모드가 비활성화된 경우에만 적용)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Klembordgeschiedenis"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipper schakelen",
|
||||
"settings": "Instellingen openen"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Klembordgeschiedenis",
|
||||
"pinned-title": "Vastgemaakte items",
|
||||
"no-pinned": "Geen vastgemaakte items",
|
||||
"settings": "Instellingen",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Zoeken...",
|
||||
"filter-all": "Alle",
|
||||
"filter-text": "Tekst",
|
||||
"filter-images": "Afbeeldingen",
|
||||
"filter-colors": "Kleuren",
|
||||
"filter-links": "Links",
|
||||
"filter-code": "Code",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Bestanden",
|
||||
"clear-all": "Alles wissen",
|
||||
"no-matches": "Geen overeenkomende items",
|
||||
"empty": "Klembord is leeg"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Toevoegen aan takenlijst",
|
||||
"pin": "Vastmaken",
|
||||
"delete": "Verwijderen"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integraties",
|
||||
"todo-integration": "ToDo-plugin-integratie",
|
||||
"todo-description": "Klembord-items rechtstreeks toevoegen aan uw takenlijst",
|
||||
"todo-disabled": "ToDo-plugin is niet geïnstalleerd of uitgeschakeld",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Kaartweergave",
|
||||
"card-type": "Kaarttype",
|
||||
"card-type-desc": "Selecteer kaarttype om aan te passen",
|
||||
"preview": "Voorbeeld",
|
||||
"sample-content": "Voorbeeldinhoud...",
|
||||
"bg-color": "Achtergrondkleur",
|
||||
"bg-color-desc": "Achtergrondkleur van kaart",
|
||||
"separator-color": "Scheidingslijnkleur",
|
||||
"separator-color-desc": "Lijn tussen koptekst en inhoud",
|
||||
"fg-color": "Voorgrondkleur",
|
||||
"fg-color-desc": "Kleur van titel, pictogrammen en tekstinhoud",
|
||||
"reset-defaults": "Standaardinstellingen herstellen",
|
||||
"panel-width": "Panelbreedte",
|
||||
"panel-width-desc": "Breedte van het paneel in pixels (alleen wanneer de volledig-schermmodus is uitgeschakeld)",
|
||||
"panel-height": "Panelhoogte",
|
||||
"panel-height-desc": "Hoogte van het paneel in pixels. Stel in op 0 voor automatische hoogte (alleen wanneer de volledig-schermmodus is uitgeschakeld)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Historia schowka"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Przełącz Clipper",
|
||||
"settings": "Otwórz ustawienia"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Historia schowka",
|
||||
"pinned-title": "Przypięte elementy",
|
||||
"no-pinned": "Brak przypiętych elementów",
|
||||
"settings": "Ustawienia",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Szukaj...",
|
||||
"filter-all": "Wszystkie",
|
||||
"filter-text": "Tekst",
|
||||
"filter-images": "Obrazy",
|
||||
"filter-colors": "Kolory",
|
||||
"filter-links": "Linki",
|
||||
"filter-code": "Kod",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Pliki",
|
||||
"clear-all": "Wyczyść wszystko",
|
||||
"no-matches": "Brak pasujących elementów",
|
||||
"empty": "Schowek jest pusty"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Dodaj do zadań",
|
||||
"pin": "Przypnij",
|
||||
"delete": "Usuń"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integracje",
|
||||
"todo-integration": "Integracja z ToDo",
|
||||
"todo-description": "Dodawaj elementy ze schowka bezpośrednio do listy zadań",
|
||||
"todo-disabled": "Plugin ToDo nie jest zainstalowany lub jest wyłączony",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Tryb pełnoekranowy",
|
||||
"fullscreen-mode-desc": "Rozwiń panel na cały ekran",
|
||||
"hide-panel-background": "Ukryj tło panelu",
|
||||
"hide-panel-background-desc": "Usuwa tło kontenerów panelu. Uwaga: Przypięte karty i notatki działają lepiej z włączonym tłem.",
|
||||
"auto-paste-section": "Automatyczne wklejanie",
|
||||
"auto-paste": "Wklej automatycznie po kliknięciu",
|
||||
"auto-paste-desc": "Automatycznie wkleja zaznaczony element do aktywnego okna",
|
||||
"auto-paste-warning": "wtype jest wymagany do automatycznego wklejania. Zainstaluj: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Tylko prawy przycisk myszy",
|
||||
"auto-paste-rmb-desc": "Prawy klik = wklej automatycznie; lewy klik = tylko kopiuj do schowka",
|
||||
"auto-paste-delay": "Opóźnienie wklejania",
|
||||
"auto-paste-delay-desc": "Opóźnienie przed wklejeniem (ms). Zwiększ jeśli włączony jest focus-follows-cursor w kompozytorze.",
|
||||
"appearance": "Wygląd kart",
|
||||
"card-type": "Typ karty",
|
||||
"card-type-desc": "Wybierz typ karty do personalizacji",
|
||||
"preview": "Podgląd",
|
||||
"sample-content": "Przykładowa treść...",
|
||||
"bg-color": "Kolor tła",
|
||||
"bg-color-desc": "Kolor tła karty",
|
||||
"separator-color": "Kolor separatora",
|
||||
"separator-color-desc": "Linia między nagłówkiem a treścią",
|
||||
"fg-color": "Kolor tekstu",
|
||||
"fg-color-desc": "Kolor tytułu, ikon i treści tekstowej",
|
||||
"reset-defaults": "Przywróć domyślne",
|
||||
"panel-width": "Szerokość panelu",
|
||||
"panel-width-desc": "Szerokość panelu w pikselach (tylko gdy tryb pełnoekranowy jest wyłączony)",
|
||||
"panel-height": "Wysokość panelu",
|
||||
"panel-height-desc": "Wysokość panelu w pikselach. Ustaw 0 dla automatycznej wysokości (tylko gdy tryb pełnoekranowy jest wyłączony)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Histórico da área de transferência"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Alternar Clipper",
|
||||
"settings": "Abrir configurações"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Histórico da área de transferência",
|
||||
"pinned-title": "Itens fixados",
|
||||
"no-pinned": "Nenhum item fixado",
|
||||
"settings": "Configurações",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Pesquisar...",
|
||||
"filter-all": "Todos",
|
||||
"filter-text": "Texto",
|
||||
"filter-images": "Imagens",
|
||||
"filter-colors": "Cores",
|
||||
"filter-links": "Links",
|
||||
"filter-code": "Código",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Arquivos",
|
||||
"clear-all": "Limpar tudo",
|
||||
"no-matches": "Nenhum item correspondente",
|
||||
"empty": "A área de transferência está vazia"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Adicionar às tarefas",
|
||||
"pin": "Fixar",
|
||||
"delete": "Excluir"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrações",
|
||||
"todo-integration": "Integração do plugin ToDo",
|
||||
"todo-description": "Adicionar itens da área de transferência diretamente à sua lista de tarefas",
|
||||
"todo-disabled": "O plugin ToDo não está instalado ou está desativado",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Aparência do cartão",
|
||||
"card-type": "Tipo de cartão",
|
||||
"card-type-desc": "Selecione o tipo de cartão para personalizar",
|
||||
"preview": "Visualização",
|
||||
"sample-content": "Visualização de conteúdo de exemplo...",
|
||||
"bg-color": "Cor de fundo",
|
||||
"bg-color-desc": "Cor de fundo do cartão",
|
||||
"separator-color": "Cor do separador",
|
||||
"separator-color-desc": "Linha entre o cabeçalho e o conteúdo",
|
||||
"fg-color": "Cor do primeiro plano",
|
||||
"fg-color-desc": "Cor do título, ícones e conteúdo de texto",
|
||||
"reset-defaults": "Redefinir para padrões",
|
||||
"panel-width": "Largura do painel",
|
||||
"panel-width-desc": "Largura do painel em pixels (apenas quando o modo ecrã completo está desativado)",
|
||||
"panel-height": "Altura do painel",
|
||||
"panel-height-desc": "Altura do painel em pixels. Defina 0 para altura automática (apenas quando o modo ecrã completo está desativado)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "История буфера обмена"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Переключить Clipper",
|
||||
"settings": "Открыть настройки"
|
||||
},
|
||||
"panel": {
|
||||
"title": "История буфера обмена",
|
||||
"pinned-title": "Закрепленные элементы",
|
||||
"no-pinned": "Нет закрепленных элементов",
|
||||
"settings": "Настройки",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Поиск...",
|
||||
"filter-all": "Все",
|
||||
"filter-text": "Текст",
|
||||
"filter-images": "Изображения",
|
||||
"filter-colors": "Цвета",
|
||||
"filter-links": "Ссылки",
|
||||
"filter-code": "Код",
|
||||
"filter-emoji": "Эмодзи",
|
||||
"filter-files": "Файлы",
|
||||
"clear-all": "Очистить все",
|
||||
"no-matches": "Нет совпадающих элементов",
|
||||
"empty": "Буфер обмена пуст"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Добавить в задачи",
|
||||
"pin": "Закрепить",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Интеграции",
|
||||
"todo-integration": "Интеграция с плагином ToDo",
|
||||
"todo-description": "Добавлять элементы буфера обмена прямо в список задач",
|
||||
"todo-disabled": "Плагин ToDo не установлен или отключен",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Внешний вид карточек",
|
||||
"card-type": "Тип карточки",
|
||||
"card-type-desc": "Выберите тип карточки для настройки",
|
||||
"preview": "Предпросмотр",
|
||||
"sample-content": "Предпросмотр образца содержимого...",
|
||||
"bg-color": "Цвет фона",
|
||||
"bg-color-desc": "Цвет фона карточки",
|
||||
"separator-color": "Цвет разделителя",
|
||||
"separator-color-desc": "Линия между заголовком и содержимым",
|
||||
"fg-color": "Цвет переднего плана",
|
||||
"fg-color-desc": "Цвет заголовка, значков и текстового содержимого",
|
||||
"reset-defaults": "Сбросить к значениям по умолчанию",
|
||||
"panel-width": "Ширина панели",
|
||||
"panel-width-desc": "Ширина панели в пикселях (только когда полноэкранный режим отключён)",
|
||||
"panel-height": "Высота панели",
|
||||
"panel-height-desc": "Высота панели в пикселях. Установите 0 для автоматической высоты (только когда полноэкранный режим отключён)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Urklippshistorik"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Växla Clipper",
|
||||
"settings": "Öppna inställningar"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Urklippshistorik",
|
||||
"pinned-title": "Fästa objekt",
|
||||
"no-pinned": "Inga fästa objekt",
|
||||
"settings": "Inställningar",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Sök...",
|
||||
"filter-all": "Alla",
|
||||
"filter-text": "Text",
|
||||
"filter-images": "Bilder",
|
||||
"filter-colors": "Färger",
|
||||
"filter-links": "Länkar",
|
||||
"filter-code": "Kod",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Filer",
|
||||
"clear-all": "Rensa allt",
|
||||
"no-matches": "Inga matchande objekt",
|
||||
"empty": "Urklippet är tomt"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Lägg till i uppgifter",
|
||||
"pin": "Fäst",
|
||||
"delete": "Ta bort"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Integrationer",
|
||||
"todo-integration": "ToDo-pluginintegration",
|
||||
"todo-description": "Lägg till urklippsobjekt direkt till din uppgiftslista",
|
||||
"todo-disabled": "ToDo-plugin är inte installerat eller inaktiverat",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Kortutseende",
|
||||
"card-type": "Korttyp",
|
||||
"card-type-desc": "Välj korttyp att anpassa",
|
||||
"preview": "Förhandsgranskning",
|
||||
"sample-content": "Förhandsgranskning av exempelinnehåll...",
|
||||
"bg-color": "Bakgrundsfärg",
|
||||
"bg-color-desc": "Kortets bakgrundsfärg",
|
||||
"separator-color": "Separatorfärg",
|
||||
"separator-color-desc": "Linje mellan rubrik och innehåll",
|
||||
"fg-color": "Förgrundsfärg",
|
||||
"fg-color-desc": "Färg på titel, ikoner och textinnehåll",
|
||||
"reset-defaults": "Återställ till standard",
|
||||
"panel-width": "Panelbredd",
|
||||
"panel-width-desc": "Panelens bredd i pixlar (bara när fullskärmsläget är inaktiverat)",
|
||||
"panel-height": "Panelhöjd",
|
||||
"panel-height-desc": "Panelens höjd i pixlar. Ange 0 för automatisk höjd (bara när fullskärmsläget är inaktiverat)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Pano geçmişi"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Clipper'ı değiştir",
|
||||
"settings": "Ayarları aç"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Pano geçmişi",
|
||||
"pinned-title": "Sabitlenmiş öğeler",
|
||||
"no-pinned": "Sabitlenmiş öğe yok",
|
||||
"settings": "Ayarlar",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Ara...",
|
||||
"filter-all": "Tümü",
|
||||
"filter-text": "Metin",
|
||||
"filter-images": "Resimler",
|
||||
"filter-colors": "Renkler",
|
||||
"filter-links": "Bağlantılar",
|
||||
"filter-code": "Kod",
|
||||
"filter-emoji": "Emoji",
|
||||
"filter-files": "Dosyalar",
|
||||
"clear-all": "Tümünü temizle",
|
||||
"no-matches": "Eşleşen öğe yok",
|
||||
"empty": "Pano boş"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Yapılacaklara ekle",
|
||||
"pin": "Sabitle",
|
||||
"delete": "Sil"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Entegrasyonlar",
|
||||
"todo-integration": "ToDo eklentisi entegrasyonu",
|
||||
"todo-description": "Pano öğelerini doğrudan yapılacaklar listenize ekleyin",
|
||||
"todo-disabled": "ToDo eklentisi yüklü değil veya devre dışı",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Kart görünümü",
|
||||
"card-type": "Kart türü",
|
||||
"card-type-desc": "Özelleştirilecek kart türünü seçin",
|
||||
"preview": "Önizleme",
|
||||
"sample-content": "Örnek içerik önizlemesi...",
|
||||
"bg-color": "Arka plan rengi",
|
||||
"bg-color-desc": "Kart arka plan rengi",
|
||||
"separator-color": "Ayırıcı rengi",
|
||||
"separator-color-desc": "Başlık ve içerik arasındaki çizgi",
|
||||
"fg-color": "Ön plan rengi",
|
||||
"fg-color-desc": "Başlık, simgeler ve metin içeriği rengi",
|
||||
"reset-defaults": "Varsayılanlara sıfırla",
|
||||
"panel-width": "Panel genişliği",
|
||||
"panel-width-desc": "Panelin piksel cinsinden genişliği (yalnızca tam ekran modu devre dışıyken)",
|
||||
"panel-height": "Panel yüksekliği",
|
||||
"panel-height-desc": "Panelin piksel cinsinden yüksekliği. Otomatik yükseklik için 0 girin (yalnızca tam ekran modu devre dışıyken)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "Історія буфера обміну"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "Перемкнути Clipper",
|
||||
"settings": "Відкрити налаштування"
|
||||
},
|
||||
"panel": {
|
||||
"title": "Історія буфера обміну",
|
||||
"pinned-title": "Закріплені елементи",
|
||||
"no-pinned": "Немає закріплених елементів",
|
||||
"settings": "Налаштування",
|
||||
"close": "Close",
|
||||
"search-placeholder": "Пошук...",
|
||||
"filter-all": "Усі",
|
||||
"filter-text": "Текст",
|
||||
"filter-images": "Зображення",
|
||||
"filter-colors": "Кольори",
|
||||
"filter-links": "Посилання",
|
||||
"filter-code": "Код",
|
||||
"filter-emoji": "Емодзі",
|
||||
"filter-files": "Файли",
|
||||
"clear-all": "Очистити все",
|
||||
"no-matches": "Немає відповідних елементів",
|
||||
"empty": "Буфер обміну порожній"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "Додати до завдань",
|
||||
"pin": "Закріпити",
|
||||
"delete": "Видалити"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "Інтеграції",
|
||||
"todo-integration": "Інтеграція з плагіном ToDo",
|
||||
"todo-description": "Додавати елементи буфера обміну безпосередньо до списку завдань",
|
||||
"todo-disabled": "Плагін ToDo не встановлено або вимкнено",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "Зовнішній вигляд карток",
|
||||
"card-type": "Тип картки",
|
||||
"card-type-desc": "Виберіть тип картки для налаштування",
|
||||
"preview": "Попередній перегляд",
|
||||
"sample-content": "Попередній перегляд зразка вмісту...",
|
||||
"bg-color": "Колір фону",
|
||||
"bg-color-desc": "Колір фону картки",
|
||||
"separator-color": "Колір роздільника",
|
||||
"separator-color-desc": "Лінія між заголовком і вмістом",
|
||||
"fg-color": "Колір переднього плану",
|
||||
"fg-color-desc": "Колір заголовка, значків і текстового вмісту",
|
||||
"reset-defaults": "Скинути до типових значень",
|
||||
"panel-width": "Ширина панелі",
|
||||
"panel-width-desc": "Ширина панелі в пікселях (тільки коли повноекранний режим вимкнено)",
|
||||
"panel-height": "Висота панелі",
|
||||
"panel-height-desc": "Висота панелі в пікселях. Встановіть 0 для автоматичної висоти (тільки коли повноекранний режим вимкнено)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "剪贴板历史"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "切换 Clipper",
|
||||
"settings": "打开设置"
|
||||
},
|
||||
"panel": {
|
||||
"title": "剪贴板历史",
|
||||
"pinned-title": "已固定项目",
|
||||
"no-pinned": "无已固定项目",
|
||||
"settings": "设置",
|
||||
"close": "Close",
|
||||
"search-placeholder": "搜索...",
|
||||
"filter-all": "全部",
|
||||
"filter-text": "文本",
|
||||
"filter-images": "图片",
|
||||
"filter-colors": "颜色",
|
||||
"filter-links": "链接",
|
||||
"filter-code": "代码",
|
||||
"filter-emoji": "表情符号",
|
||||
"filter-files": "文件",
|
||||
"clear-all": "全部清除",
|
||||
"no-matches": "无匹配项目",
|
||||
"empty": "剪贴板为空"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "添加到待办事项",
|
||||
"pin": "固定",
|
||||
"delete": "删除"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "集成",
|
||||
"todo-integration": "ToDo 插件集成",
|
||||
"todo-description": "将剪贴板项目直接添加到待办事项列表",
|
||||
"todo-disabled": "ToDo 插件未安装或已禁用",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "卡片外观",
|
||||
"card-type": "卡片类型",
|
||||
"card-type-desc": "选择要自定义的卡片类型",
|
||||
"preview": "预览",
|
||||
"sample-content": "示例内容预览...",
|
||||
"bg-color": "背景颜色",
|
||||
"bg-color-desc": "卡片背景颜色",
|
||||
"separator-color": "分隔线颜色",
|
||||
"separator-color-desc": "标题和内容之间的线条",
|
||||
"fg-color": "前景颜色",
|
||||
"fg-color-desc": "标题、图标和文本内容颜色",
|
||||
"reset-defaults": "重置为默认值",
|
||||
"panel-width": "面板宽度",
|
||||
"panel-width-desc": "面板的像素宽度(仅在全屏模式关闭时有效)",
|
||||
"panel-height": "面板高度",
|
||||
"panel-height-desc": "面板的像素高度。设为 0 则自动高度(仅在全屏模式关闭时有效)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"bar": {
|
||||
"tooltip": "剪貼簿歷史記錄"
|
||||
},
|
||||
"context": {
|
||||
"toggle": "切換 Clipper",
|
||||
"settings": "開啟設定"
|
||||
},
|
||||
"panel": {
|
||||
"title": "剪貼簿歷史記錄",
|
||||
"pinned-title": "已釘選項目",
|
||||
"no-pinned": "無已釘選項目",
|
||||
"settings": "設定",
|
||||
"close": "Close",
|
||||
"search-placeholder": "搜尋...",
|
||||
"filter-all": "全部",
|
||||
"filter-text": "文字",
|
||||
"filter-images": "圖片",
|
||||
"filter-colors": "顏色",
|
||||
"filter-links": "連結",
|
||||
"filter-code": "程式碼",
|
||||
"filter-emoji": "表情符號",
|
||||
"filter-files": "檔案",
|
||||
"clear-all": "全部清除",
|
||||
"no-matches": "無符合項目",
|
||||
"empty": "剪貼簿為空"
|
||||
},
|
||||
"card": {
|
||||
"add-todo": "加入待辦事項",
|
||||
"pin": "釘選",
|
||||
"delete": "刪除"
|
||||
},
|
||||
"notecards": {
|
||||
"empty-state": "No notes yet",
|
||||
"empty-hint": "Click the button below to create your first note",
|
||||
"create-note": "Create Note",
|
||||
"change-color": "Change Color",
|
||||
"export": "Export to .txt",
|
||||
"delete": "Delete Note",
|
||||
"untitled-placeholder": "Untitled"
|
||||
},
|
||||
"toast": {
|
||||
"invalid-clipboard-item": "Invalid clipboard item",
|
||||
"max-pinned-items": "Maximum {max} pinned items reached",
|
||||
"item-not-found": "Item not found in clipboard",
|
||||
"failed-to-pin": "Failed to pin item",
|
||||
"failed-to-pin-image": "Failed to pin image",
|
||||
"image-too-large": "Image too large to pin (max 5MB)",
|
||||
"text-too-large": "Text too large to pin (max 1MB)",
|
||||
"item-pinned": "Item pinned",
|
||||
"item-unpinned": "Item unpinned",
|
||||
"max-notes": "Maximum {max} notes reached",
|
||||
"note-created": "Note created",
|
||||
"note-deleted": "Note deleted",
|
||||
"note-not-found": "Note not found",
|
||||
"note-exported": "Note exported to ~/Documents/{fileName}",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
"failed-to-copy-image": "Failed to copy image",
|
||||
"failed-to-copy-text": "Failed to copy text",
|
||||
"no-text-selected": "No text selected",
|
||||
"failed-to-get-selection": "Failed to get selection",
|
||||
"no-text-to-add": "No text to add",
|
||||
"todo-not-available": "ToDo plugin not available",
|
||||
"added-to-todo": "Added to ToDo",
|
||||
"failed-to-copy": "Failed to copy to clipboard",
|
||||
"todo-disabled": "ToDo integration is disabled",
|
||||
"could-not-open-todo": "Could not open ToDo selector",
|
||||
"could-not-open-note-selector": "Could not open note selector",
|
||||
"text-added-to-note": "Text added to note",
|
||||
"todo-page-created": "New ToDo page created",
|
||||
"failed-to-get-cursor-position": "Failed to get cursor position",
|
||||
"notes-cleared": "All notes cleared",
|
||||
"pinned-cleared": "All pinned items cleared"
|
||||
},
|
||||
"settings": {
|
||||
"tab-general": "General",
|
||||
"tab-appearance": "Appearance",
|
||||
"integrations": "整合",
|
||||
"todo-integration": "ToDo 外掛程式整合",
|
||||
"todo-description": "將剪貼簿項目直接加入待辦事項清單",
|
||||
"todo-disabled": "ToDo 外掛程式未安裝或已停用",
|
||||
"features": "Features",
|
||||
"pincards-enabled": "Enable Pin Cards",
|
||||
"pincards-desc": "Show pinned items panel and allow pinning clipboard items",
|
||||
"pincards-items-count": "Pinned Items",
|
||||
"clear-all-pinned": "Clear All Pinned Items",
|
||||
"notecards-enabled": "Enable NoteCards",
|
||||
"notecards-desc": "Show notecards panel for quick notes",
|
||||
"notecards-notes-count": "Current Notes",
|
||||
"clear-all-notes": "Clear All Notes",
|
||||
"show-close-button": "Show Close Button",
|
||||
"show-close-button-desc": "Display an X button at the top-right corner to close the panel",
|
||||
"fullscreen-mode": "Fullscreen Mode",
|
||||
"fullscreen-mode-desc": "Expand the clipboard panel to fill the entire screen",
|
||||
"hide-panel-background": "Hide Panel Background",
|
||||
"hide-panel-background-desc": "Remove background from panel containers. Note: Pin Cards and Note Cards work best with background enabled.",
|
||||
"auto-paste-section": "Auto-Paste",
|
||||
"auto-paste": "Auto-Paste on Click",
|
||||
"auto-paste-desc": "Automatically paste clipboard item into focused window after selecting",
|
||||
"auto-paste-warning": "wtype is required for auto-paste. Install with: sudo pacman -S wtype",
|
||||
"auto-paste-rmb": "Right-Click Only",
|
||||
"auto-paste-rmb-desc": "Use right-click for auto-paste; left-click copies to clipboard without pasting",
|
||||
"auto-paste-delay": "Paste Delay",
|
||||
"auto-paste-delay-desc": "Delay before pasting (ms). Increase if focus follows cursor is enabled in your compositor.",
|
||||
"appearance": "卡片外觀",
|
||||
"card-type": "卡片類型",
|
||||
"card-type-desc": "選擇要自訂的卡片類型",
|
||||
"preview": "預覽",
|
||||
"sample-content": "範例內容預覽...",
|
||||
"bg-color": "背景顏色",
|
||||
"bg-color-desc": "卡片背景顏色",
|
||||
"separator-color": "分隔線顏色",
|
||||
"separator-color-desc": "標題與內容之間的線條",
|
||||
"fg-color": "前景顏色",
|
||||
"fg-color-desc": "標題、圖示和文字內容顏色",
|
||||
"reset-defaults": "重設為預設值",
|
||||
"panel-width": "面板寬度",
|
||||
"panel-width-desc": "面板的像素寬度(僅在全螢幕模式關閉時有效)",
|
||||
"panel-height": "面板高度",
|
||||
"panel-height-desc": "面板的像素高度。設為 0 則自動高度(僅在全螢幕模式關閉時有效)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "clipper",
|
||||
"name": "Clipper",
|
||||
"version": "2.4.4",
|
||||
"minNoctaliaVersion": "4.1.2",
|
||||
"author": "blackbartblues",
|
||||
"contributors": [
|
||||
"rscipher001",
|
||||
"neyfua"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Advanced clipboard manager with history, search, keyboard navigation, ToDo integration, pinned items, notecards/sticky notes, and selection-to-notecard feature. Fully translated with comprehensive i18n support.",
|
||||
"tags": [
|
||||
"Utility",
|
||||
"Bar",
|
||||
"Panel"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"panel": "Panel.qml",
|
||||
"settings": "Settings.qml",
|
||||
"controlCenterWidget": "ControlCenterWidget.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"enableTodoIntegration": false,
|
||||
"pincardsEnabled": true,
|
||||
"notecardsEnabled": true,
|
||||
"showCloseButton": true,
|
||||
"fullscreenMode": false,
|
||||
"hidePanelBackground": false,
|
||||
"autoPaste": false,
|
||||
"autoPasteOnRightClick": false,
|
||||
"autoPasteDelay": 300,
|
||||
"panelWidth": 1450,
|
||||
"panelHeight": 0,
|
||||
"cardColors": {},
|
||||
"customColors": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.3 MiB |
@@ -0,0 +1,150 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.DesktopWidgets
|
||||
import qs.Services.Media
|
||||
|
||||
DraggableDesktopWidget {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
implicitWidth: Math.round(300 * widgetScale)
|
||||
implicitHeight: Math.round(300 * widgetScale)
|
||||
|
||||
showBackground: false
|
||||
|
||||
// Scaled dimensions
|
||||
readonly property int scaledRadiusL: Math.round(Style.radiusL * widgetScale)
|
||||
|
||||
// Settings from plugin
|
||||
readonly property real sensitivity: widgetData?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? pluginApi?.manifest?.metadata?.defaultSettings?.sensitivity
|
||||
readonly property real rotationSpeed: widgetData?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? pluginApi?.manifest?.metadata?.defaultSettings?.rotationSpeed
|
||||
readonly property real barWidth: widgetData?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? pluginApi?.manifest?.metadata?.defaultSettings?.barWidth
|
||||
readonly property real ringOpacity: widgetData?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? pluginApi?.manifest?.metadata?.defaultSettings?.ringOpacity
|
||||
readonly property real bloomIntensity: widgetData?.bloomIntensity ?? pluginApi.pluginSettings?.bloomIntensity ?? pluginApi?.manifest?.metadata?.defaultSettings?.bloomIntensity
|
||||
readonly property int visualizationMode: widgetData?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? pluginApi?.manifest?.metadata?.defaultSettings?.visualizationMode ?? 3
|
||||
readonly property real waveThickness: widgetData?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? pluginApi?.manifest?.metadata?.defaultSettings?.waveThickness ?? 1.0
|
||||
readonly property real innerDiameter: widgetData?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? pluginApi?.manifest?.metadata?.defaultSettings?.innerDiameter ?? 0.7
|
||||
readonly property bool fadeWhenIdle: widgetData?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? false
|
||||
readonly property bool useCustomColors: widgetData?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
|
||||
readonly property color customPrimaryColor: widgetData?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
|
||||
readonly property color customSecondaryColor: widgetData?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
|
||||
|
||||
// Animation time for shader (0 to 3600, 1 hour cycle)
|
||||
property real shaderTime: 0
|
||||
NumberAnimation on shaderTime {
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 3600
|
||||
duration: 3600000
|
||||
running: !SpectrumService.isIdle
|
||||
}
|
||||
|
||||
// Hidden canvas that encodes audio data as a 32x1 texture
|
||||
Canvas {
|
||||
id: audioCanvas
|
||||
width: 32
|
||||
height: 1
|
||||
visible: false
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
var values = SpectrumService.values;
|
||||
if (!values || values.length === 0) {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillRect(0, 0, 32, 1);
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < 32; i++) {
|
||||
var v = values[i] || 0;
|
||||
// Encode amplitude in grayscale (R=G=B=amplitude)
|
||||
var c = Math.floor(v * 255);
|
||||
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
|
||||
ctx.fillRect(i, 0, 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger canvas repaint when audio data changes
|
||||
Connections {
|
||||
target: SpectrumService
|
||||
function onValuesChanged() {
|
||||
if (!SpectrumService.isIdle) {
|
||||
audioCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unique instance ID for SpectrumService registration
|
||||
// This prevents the old widget's destruction from unregistering the new widget
|
||||
readonly property string spectrumInstanceId: "plugin:fancy-audiovisualizer:" + Date.now() + Math.random()
|
||||
|
||||
// Register with SpectrumService when pluginApi becomes available
|
||||
onPluginApiChanged: {
|
||||
if (pluginApi) {
|
||||
SpectrumService.registerComponent(spectrumInstanceId);
|
||||
audioCanvas.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
SpectrumService.unregisterComponent(spectrumInstanceId);
|
||||
}
|
||||
|
||||
// Audio texture source (outside ShaderEffect to avoid 'source' property warning)
|
||||
ShaderEffectSource {
|
||||
id: audioTextureSource
|
||||
sourceItem: audioCanvas
|
||||
live: true
|
||||
hideSource: true
|
||||
}
|
||||
|
||||
// The shader effect visualization
|
||||
ShaderEffect {
|
||||
id: visualizer
|
||||
anchors.fill: parent
|
||||
visible: pluginApi !== null
|
||||
opacity: (root.fadeWhenIdle && SpectrumService.isIdle) ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 500; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
// Audio texture - named 'source' to match ShaderEffectSource's property and avoid warning
|
||||
property var source: audioTextureSource
|
||||
|
||||
// Uniforms passed to shader
|
||||
property real time: root.shaderTime
|
||||
property real itemWidth: visualizer.width
|
||||
property real itemHeight: visualizer.height
|
||||
property color primaryColor: root.useCustomColors ? root.customPrimaryColor : Color.mPrimary
|
||||
property color secondaryColor: root.useCustomColors ? root.customSecondaryColor : Color.mSecondary
|
||||
property real sensitivity: root.sensitivity
|
||||
property real rotationSpeed: root.rotationSpeed
|
||||
property real barWidth: root.barWidth
|
||||
property real ringOpacity: root.ringOpacity
|
||||
property real cornerRadius: scaledRadiusL
|
||||
property real bloomIntensity: root.bloomIntensity
|
||||
property real visualizationMode: root.visualizationMode
|
||||
property real waveThickness: root.waveThickness
|
||||
property real innerDiameter: root.innerDiameter
|
||||
|
||||
fragmentShader: pluginApi ? Qt.resolvedUrl(pluginApi.pluginDir + "/shaders/visualizer.frag.qsb") : ""
|
||||
}
|
||||
|
||||
// Fallback when shader not loaded
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Color.mSurface
|
||||
radius: scaledRadiusL
|
||||
visible: !visualizer.visible || visualizer.fragmentShader === ""
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Loading..."
|
||||
color: Color.mOnSurface
|
||||
font.pointSize: Math.round(Style.fontSizeM * widgetScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property var widgetSettings: null
|
||||
property var screen: null
|
||||
|
||||
spacing: Style.marginM
|
||||
|
||||
// Local state for editing
|
||||
property real valueSensitivity: widgetSettings?.data?.sensitivity ?? pluginApi?.pluginSettings?.sensitivity ?? 1.5
|
||||
property real valueRotationSpeed: widgetSettings?.data?.rotationSpeed ?? pluginApi?.pluginSettings?.rotationSpeed ?? 0.5
|
||||
property real valueBarWidth: widgetSettings?.data?.barWidth ?? pluginApi?.pluginSettings?.barWidth ?? 0.6
|
||||
property real valueRingOpacity: widgetSettings?.data?.ringOpacity ?? pluginApi?.pluginSettings?.ringOpacity ?? 0.8
|
||||
property real valueBloomIntensity: widgetSettings?.data?.bloomIntensity ?? pluginApi?.pluginSettings?.bloomIntensity ?? 0.5
|
||||
property int valueVisualizationMode: widgetSettings?.data?.visualizationMode ?? pluginApi?.pluginSettings?.visualizationMode ?? 3
|
||||
property real valueWaveThickness: widgetSettings?.data?.waveThickness ?? pluginApi?.pluginSettings?.waveThickness ?? 1.0
|
||||
property real valueInnerDiameter: widgetSettings?.data?.innerDiameter ?? pluginApi?.pluginSettings?.innerDiameter ?? 0.7
|
||||
property bool valueFadeWhenIdle: widgetSettings?.data?.fadeWhenIdle ?? pluginApi?.pluginSettings?.fadeWhenIdle ?? true
|
||||
property bool valueUseCustomColors: widgetSettings?.data?.useCustomColors ?? pluginApi?.pluginSettings?.useCustomColors ?? false
|
||||
property color valueCustomPrimaryColor: widgetSettings?.data?.customPrimaryColor ?? pluginApi?.pluginSettings?.customPrimaryColor ?? "#6750A4"
|
||||
property color valueCustomSecondaryColor: widgetSettings?.data?.customSecondaryColor ?? pluginApi?.pluginSettings?.customSecondaryColor ?? "#625B71"
|
||||
|
||||
// Mode helpers
|
||||
readonly property bool modeHasBars: valueVisualizationMode === 0 || valueVisualizationMode === 3 || valueVisualizationMode === 5
|
||||
readonly property bool modeHasWave: valueVisualizationMode === 1 || valueVisualizationMode === 4 || valueVisualizationMode === 5
|
||||
readonly property bool modeHasRings: valueVisualizationMode >= 2
|
||||
|
||||
NHeader {
|
||||
label: pluginApi?.tr("settings.title") ?? "Visualizer Settings"
|
||||
description: pluginApi?.tr("settings.description") ?? "Configure the audio visualizer appearance"
|
||||
}
|
||||
|
||||
// Visualization mode selector
|
||||
NComboBox {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.visualizationMode") ?? "Visualization Mode"
|
||||
description: pluginApi?.tr("settings.visualizationMode-description") ?? "Choose visualization style"
|
||||
model: [
|
||||
{"key": "0", "name": pluginApi?.tr("settings.mode.bars") ?? "Bars"},
|
||||
{"key": "1", "name": pluginApi?.tr("settings.mode.wave") ?? "Wave"},
|
||||
{"key": "2", "name": pluginApi?.tr("settings.mode.rings") ?? "Rings"},
|
||||
{"key": "3", "name": pluginApi?.tr("settings.mode.barsRings") ?? "Bars + Rings"},
|
||||
{"key": "4", "name": pluginApi?.tr("settings.mode.waveRings") ?? "Wave + Rings"},
|
||||
{"key": "5", "name": pluginApi?.tr("settings.mode.all") ?? "All"}
|
||||
]
|
||||
currentKey: String(root.valueVisualizationMode)
|
||||
onSelected: key => {
|
||||
root.valueVisualizationMode = parseInt(key);
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Wave thickness slider (shown when mode includes wave)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasWave
|
||||
label: pluginApi?.tr("settings.waveThickness") ?? "Wave Thickness"
|
||||
value: root.valueWaveThickness
|
||||
from: 0.3
|
||||
to: 2.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueWaveThickness = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Sensitivity slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.sensitivity") ?? "Sensitivity"
|
||||
value: root.valueSensitivity
|
||||
from: 0.5
|
||||
to: 3.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueSensitivity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Rotation speed slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.rotationSpeed") ?? "Rotation Speed"
|
||||
value: root.valueRotationSpeed
|
||||
from: 0.0
|
||||
to: 2.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueRotationSpeed = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Bar width slider (shown when mode includes bars)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasBars
|
||||
label: pluginApi?.tr("settings.barWidth") ?? "Bar Width"
|
||||
value: root.valueBarWidth
|
||||
from: 0.2
|
||||
to: 1.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueBarWidth = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Ring opacity slider (shown when mode includes rings)
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
visible: root.modeHasRings
|
||||
label: pluginApi?.tr("settings.ringOpacity") ?? "Ring Opacity"
|
||||
value: root.valueRingOpacity
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.1
|
||||
onMoved: value => {
|
||||
root.valueRingOpacity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Base diameter slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.innerDiameter") ?? "Inner Diameter"
|
||||
value: root.valueInnerDiameter
|
||||
from: 0
|
||||
to: 1
|
||||
stepSize: 0.05
|
||||
onMoved: value => {
|
||||
root.valueInnerDiameter = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Bloom intensity slider
|
||||
NValueSlider {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.bloomIntensity") ?? "Bloom Intensity"
|
||||
value: root.valueBloomIntensity
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
stepSize: 0.05
|
||||
onMoved: value => {
|
||||
root.valueBloomIntensity = value;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Fade when idle toggle
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.fadeWhenIdle") ?? "Fade When Idle"
|
||||
description: pluginApi?.tr("settings.fadeWhenIdle-description") ?? "Fade out visualizer when no audio is playing"
|
||||
checked: root.valueFadeWhenIdle
|
||||
onToggled: checked => {
|
||||
root.valueFadeWhenIdle = checked;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Use custom colors toggle
|
||||
NToggle {
|
||||
label: pluginApi?.tr("settings.useCustomColors") ?? "Use Custom Colors"
|
||||
description: pluginApi?.tr("settings.useCustomColors-description") ?? "Override theme colors with custom colors"
|
||||
checked: root.valueUseCustomColors
|
||||
onToggled: checked => {
|
||||
root.valueUseCustomColors = checked;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom primary color picker
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.valueUseCustomColors
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.customPrimaryColor") ?? "Primary Color"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: Screen
|
||||
selectedColor: root.valueCustomPrimaryColor
|
||||
onColorSelected: color => {
|
||||
root.valueCustomPrimaryColor = color;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom secondary color picker
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: root.valueUseCustomColors
|
||||
spacing: Style.marginM
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("settings.customSecondaryColor") ?? "Secondary Color"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NColorPicker {
|
||||
screen: Screen
|
||||
selectedColor: root.valueCustomSecondaryColor
|
||||
onColorSelected: color => {
|
||||
root.valueCustomSecondaryColor = color;
|
||||
root.saveSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when user clicks Apply/Save
|
||||
function saveSettings() {
|
||||
if (!widgetSettings)
|
||||
return;
|
||||
|
||||
widgetSettings.data.sensitivity = root.valueSensitivity;
|
||||
widgetSettings.data.rotationSpeed = root.valueRotationSpeed;
|
||||
widgetSettings.data.barWidth = root.valueBarWidth;
|
||||
widgetSettings.data.ringOpacity = root.valueRingOpacity;
|
||||
widgetSettings.data.bloomIntensity = root.valueBloomIntensity;
|
||||
widgetSettings.data.visualizationMode = root.valueVisualizationMode;
|
||||
widgetSettings.data.waveThickness = root.valueWaveThickness;
|
||||
widgetSettings.data.innerDiameter = root.valueInnerDiameter;
|
||||
widgetSettings.data.fadeWhenIdle = root.valueFadeWhenIdle;
|
||||
widgetSettings.data.useCustomColors = root.valueUseCustomColors;
|
||||
widgetSettings.data.customPrimaryColor = root.valueCustomPrimaryColor.toString();
|
||||
widgetSettings.data.customSecondaryColor = root.valueCustomSecondaryColor.toString();
|
||||
|
||||
widgetSettings.save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# Fancy Audiovisualizer
|
||||
|
||||
A circular audio visualizer desktop widget for Noctalia Shell with shader-based rendering and multiple visualization modes.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Visualization Modes**: Bars, Wave, Rings, or combinations (Bars+Rings, Wave+Rings, All)
|
||||
- **Shader-Based Rendering**: Smooth, GPU-accelerated visualization using custom fragment shaders
|
||||
- **Theme Integration**: Automatically uses Noctalia theme colors, with optional custom color override
|
||||
- **Configurable Appearance**: Adjust sensitivity, rotation speed, bar width, ring opacity, bloom intensity, and more
|
||||
- **Idle Fade**: Optional fade-out when no audio is playing
|
||||
|
||||
## Installation
|
||||
|
||||
This plugin is part of the `noctalia-plugins` repository.
|
||||
|
||||
## Configuration
|
||||
|
||||
Access the plugin settings in Noctalia to configure the following options:
|
||||
|
||||
- **Visualization Mode**: Choose between Bars, Wave, Rings, Bars+Rings, Wave+Rings, or All
|
||||
- **Wave Thickness**: Thickness of the wave visualization (when enabled)
|
||||
- **Sensitivity**: Audio response sensitivity (0.5 - 3.0)
|
||||
- **Rotation Speed**: Speed of visualization rotation (0.0 - 2.0)
|
||||
- **Bar Width**: Width of audio bars (when enabled)
|
||||
- **Ring Opacity**: Opacity of background rings (when enabled)
|
||||
- **Inner Diameter**: Size of the inner empty area
|
||||
- **Bloom Intensity**: Glow/bloom effect strength
|
||||
- **Fade When Idle**: Fade out when no audio is playing
|
||||
- **Custom Colors**: Override theme colors with custom primary and secondary colors
|
||||
|
||||
## Usage
|
||||
|
||||
Add the widget to your desktop via the Noctalia desktop widgets interface. The visualizer responds to system audio.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Noctalia 3.7.2 or later
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Balkenbreite",
|
||||
"bloomIntensity": "Blütenintensität",
|
||||
"customPrimaryColor": "Primärfarbe",
|
||||
"customSecondaryColor": "Sekundärfarbe",
|
||||
"description": "Audiovisualisierungsdarstellung konfigurieren",
|
||||
"fadeWhenIdle": "Ausblenden bei Inaktivität",
|
||||
"fadeWhenIdle-description": "Visualisierer ausblenden, wenn keine Audioausgabe erfolgt",
|
||||
"innerDiameter": "Innendurchmesser",
|
||||
"mode": {
|
||||
"all": "Alle",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Barren + Ringe",
|
||||
"rings": "Ringe",
|
||||
"wave": "Welle",
|
||||
"waveRings": "Welle + Ringe"
|
||||
},
|
||||
"ringOpacity": "Ring-Opazität",
|
||||
"rotationSpeed": "Drehzahl",
|
||||
"sensitivity": "Empfindlichkeit",
|
||||
"title": "Visualisierungseinstellungen",
|
||||
"useCustomColors": "Benutzerdefinierte Farben verwenden",
|
||||
"useCustomColors-description": "Themenfarben mit benutzerdefinierten Farben überschreiben",
|
||||
"visualizationMode": "Visualisierungsmodus",
|
||||
"visualizationMode-description": "Visualisierungsstil wählen",
|
||||
"waveThickness": "Wellendicke"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Bar Width",
|
||||
"bloomIntensity": "Bloom Intensity",
|
||||
"customPrimaryColor": "Primary Color",
|
||||
"customSecondaryColor": "Secondary Color",
|
||||
"description": "Configure the audio visualizer appearance",
|
||||
"fadeWhenIdle": "Fade When Idle",
|
||||
"fadeWhenIdle-description": "Fade out visualizer when no audio is playing",
|
||||
"innerDiameter": "Inner Diameter",
|
||||
"mode": {
|
||||
"all": "All",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Bars + Rings",
|
||||
"rings": "Rings",
|
||||
"wave": "Wave",
|
||||
"waveRings": "Wave + Rings"
|
||||
},
|
||||
"ringOpacity": "Ring Opacity",
|
||||
"rotationSpeed": "Rotation Speed",
|
||||
"sensitivity": "Sensitivity",
|
||||
"title": "Visualizer Settings",
|
||||
"useCustomColors": "Use Custom Colors",
|
||||
"useCustomColors-description": "Override theme colors with custom colors",
|
||||
"visualizationMode": "Visualization Mode",
|
||||
"visualizationMode-description": "Choose visualization style",
|
||||
"waveThickness": "Wave Thickness"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ancho de barra",
|
||||
"bloomIntensity": "Intensidad de floración",
|
||||
"customPrimaryColor": "Color primario",
|
||||
"customSecondaryColor": "Color secundario",
|
||||
"description": "Configurar la apariencia del visualizador de audio",
|
||||
"fadeWhenIdle": "Desvanecer al estar inactivo",
|
||||
"fadeWhenIdle-description": "Atenuar el visualizador cuando no se reproduce audio.",
|
||||
"innerDiameter": "Diámetro interior",
|
||||
"mode": {
|
||||
"all": "Todo",
|
||||
"bars": "Barras",
|
||||
"barsRings": "Barras + Anillas",
|
||||
"rings": "Anillos",
|
||||
"wave": "Ola",
|
||||
"waveRings": "Onda + Anillos"
|
||||
},
|
||||
"ringOpacity": "Opacidad del anillo",
|
||||
"rotationSpeed": "Velocidad de rotación",
|
||||
"sensitivity": "Sensibilidad",
|
||||
"title": "Ajustes del Visualizador",
|
||||
"useCustomColors": "Usar colores personalizados",
|
||||
"useCustomColors-description": "Anular los colores del tema con colores personalizados",
|
||||
"visualizationMode": "Modo de visualización",
|
||||
"visualizationMode-description": "Elegir estilo de visualización",
|
||||
"waveThickness": "Espesor de la onda"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Largeur de la barre",
|
||||
"bloomIntensity": "Intensité de l'éclat",
|
||||
"customPrimaryColor": "Couleur primaire",
|
||||
"customSecondaryColor": "Couleur secondaire",
|
||||
"description": "Configurer l'apparence du visualiseur audio",
|
||||
"fadeWhenIdle": "Estomper en cas d'inactivité",
|
||||
"fadeWhenIdle-description": "Estomper le visualiseur en l'absence de son.",
|
||||
"innerDiameter": "Diamètre intérieur",
|
||||
"mode": {
|
||||
"all": "Tout",
|
||||
"bars": "Barres",
|
||||
"barsRings": "Barres + Anneaux",
|
||||
"rings": "Anneaux",
|
||||
"wave": "Vague",
|
||||
"waveRings": "Onde + Anneaux"
|
||||
},
|
||||
"ringOpacity": "Opacité de l'anneau",
|
||||
"rotationSpeed": "Vitesse de rotation",
|
||||
"sensitivity": "Sensibilité",
|
||||
"title": "Paramètres du visualiseur",
|
||||
"useCustomColors": "Utiliser des couleurs personnalisées",
|
||||
"useCustomColors-description": "Remplacer les couleurs du thème par des couleurs personnalisées",
|
||||
"visualizationMode": "Mode de visualisation",
|
||||
"visualizationMode-description": "Choisir le style de visualisation",
|
||||
"waveThickness": "Épaisseur de la vague"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Sávszélesség",
|
||||
"bloomIntensity": "Virágzás intenzitása",
|
||||
"customPrimaryColor": "Elsődleges szín",
|
||||
"customSecondaryColor": "Másodlagos szín",
|
||||
"description": "Az audio vizualizáló megjelenésének beállítása",
|
||||
"fadeWhenIdle": "Halványítás tétlenség esetén",
|
||||
"fadeWhenIdle-description": "A vizualizáció elhalványítása, ha nincs hang lejátszva",
|
||||
"innerDiameter": "Belső átmérő",
|
||||
"mode": {
|
||||
"all": "Összes",
|
||||
"bars": "Sávok",
|
||||
"barsRings": "Sávok + Gyűrűk",
|
||||
"rings": "Gyűrűk",
|
||||
"wave": "Hullám",
|
||||
"waveRings": "Hullám + Gyűrűk"
|
||||
},
|
||||
"ringOpacity": "Gyűrű átlátszósága",
|
||||
"rotationSpeed": "Forgási sebesség",
|
||||
"sensitivity": "Érzékenység",
|
||||
"title": "Vizualizációs beállítások",
|
||||
"useCustomColors": "Egyéni színek használata",
|
||||
"useCustomColors-description": "A téma színeinek felülírása egyéni színekkel",
|
||||
"visualizationMode": "Megjelenítési mód",
|
||||
"visualizationMode-description": "Válassz megjelenítési stílust",
|
||||
"waveThickness": "Hullámvastagság"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Breite der Leiste",
|
||||
"bloomIntensity": "Intensità Bloom",
|
||||
"customPrimaryColor": "Ngjyra parësore",
|
||||
"customSecondaryColor": "Ngjyra dytësore",
|
||||
"description": "Konfigurieren Sie das Aussehen des Audio-Visualisierers.",
|
||||
"fadeWhenIdle": "Verblassen im Leerlauf",
|
||||
"fadeWhenIdle-description": "Vizualizátor eltüntetése, ha nincs hang lejátszva.",
|
||||
"innerDiameter": "Innendurchmesser",
|
||||
"mode": {
|
||||
"all": "Kaikki",
|
||||
"bars": "Pritličja",
|
||||
"barsRings": "Stangen + Ringe",
|
||||
"rings": "Hringar",
|
||||
"wave": "Val",
|
||||
"waveRings": "Welle + Ringe"
|
||||
},
|
||||
"ringOpacity": "Neprůhlednost prstence",
|
||||
"rotationSpeed": "Schnelllaufdrehzahl",
|
||||
"sensitivity": "Ndjeshmëri",
|
||||
"title": "Mga Setting ng Visualizer",
|
||||
"useCustomColors": "Përdorni Ngjyra të Personalizuara",
|
||||
"useCustomColors-description": "Ngjyrat e personalizuara mbivendosin ngjyrat e temës",
|
||||
"visualizationMode": "Módszer megjelenítése",
|
||||
"visualizationMode-description": "Zgjidhni stilin e vizualizimit",
|
||||
"waveThickness": "Onda-lodiera"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "バーの幅",
|
||||
"bloomIntensity": "開花強度",
|
||||
"customPrimaryColor": "原色",
|
||||
"customSecondaryColor": "二次色",
|
||||
"description": "オーディオビジュアライザーの外観を設定します。",
|
||||
"fadeWhenIdle": "アイドル時にフェード",
|
||||
"fadeWhenIdle-description": "音声が再生されていないときは、ビジュアライザーをフェードアウトする",
|
||||
"innerDiameter": "内径",
|
||||
"mode": {
|
||||
"all": "すべて",
|
||||
"bars": "バー",
|
||||
"barsRings": "鉄棒と輪",
|
||||
"rings": "リング",
|
||||
"wave": "波",
|
||||
"waveRings": "波紋 + リング"
|
||||
},
|
||||
"ringOpacity": "リングの不透明度",
|
||||
"rotationSpeed": "回転速度",
|
||||
"sensitivity": "感度",
|
||||
"title": "ビジュアライザー設定",
|
||||
"useCustomColors": "カスタムカラーを使用する",
|
||||
"useCustomColors-description": "カスタムカラーでテーマの色をオーバーライドする",
|
||||
"visualizationMode": "可視化モード",
|
||||
"visualizationMode-description": "視覚化のスタイルを選択",
|
||||
"waveThickness": "波厚"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Firehiya Barê",
|
||||
"bloomIntensity": "Zehfîya Gurtî",
|
||||
"customPrimaryColor": "Rengê Seretayî",
|
||||
"customSecondaryColor": "Rengê Duyemîn",
|
||||
"description": "Xuyakirina dîmenê bihîstwerî yê dengî",
|
||||
"fadeWhenIdle": "Dema de bêdengiyê vemirîne",
|
||||
"fadeWhenIdle-description": "Dîmenkerê dema ku tu deng lê nabe, hêdî hêdî winda bike",
|
||||
"innerDiameter": "Dirêjbûna Hundir",
|
||||
"mode": {
|
||||
"all": "Hemû",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Bars + Rings",
|
||||
"rings": "Xelqet",
|
||||
"wave": "Pêl",
|
||||
"waveRings": "Pêl + Xelek"
|
||||
},
|
||||
"ringOpacity": "Ronahiya Zengil",
|
||||
"rotationSpeed": "Leza Şewitandinê",
|
||||
"sensitivity": "Hesasiyet",
|
||||
"title": "Mîhengên Dîmenderê",
|
||||
"useCustomColors": "Rengên Xweser Bikar Bîne",
|
||||
"useCustomColors-description": "Rengên mijarê bi rengên xwerû biguherîne",
|
||||
"visualizationMode": "Şêwaza Dîmenkirinê",
|
||||
"visualizationMode-description": "Şêwaza dîtinê hilbijêre",
|
||||
"waveThickness": "Qalinîya Pêlê"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Balkbreedte",
|
||||
"bloomIntensity": "Bloei Intensiteit",
|
||||
"customPrimaryColor": "Primaire kleur",
|
||||
"customSecondaryColor": "Secundaire kleur",
|
||||
"description": "Configureer de weergave van de audio visualisatie.",
|
||||
"fadeWhenIdle": "Vervagen bij inactiviteit",
|
||||
"fadeWhenIdle-description": "Visualiseerder laten vervagen wanneer er geen audio wordt afgespeeld.",
|
||||
"innerDiameter": "Binnendiameter",
|
||||
"mode": {
|
||||
"all": "Alles",
|
||||
"bars": "Bars",
|
||||
"barsRings": "Rekstok + Ringen",
|
||||
"rings": "Ringen",
|
||||
"wave": "Golf",
|
||||
"waveRings": "Golf + Ringen"
|
||||
},
|
||||
"ringOpacity": "Ringondoorzichtigheid",
|
||||
"rotationSpeed": "Rotatiesnelheid",
|
||||
"sensitivity": "Gevoeligheid",
|
||||
"title": "Visualisatie-instellingen",
|
||||
"useCustomColors": "Aangepaste kleuren gebruiken",
|
||||
"useCustomColors-description": "Themakleuren overschrijven met aangepaste kleuren",
|
||||
"visualizationMode": "Visualisatiemodus",
|
||||
"visualizationMode-description": "Kies visualisatiestijl",
|
||||
"waveThickness": "Golflaagdikte"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Szerokość paska",
|
||||
"bloomIntensity": "Intensywność Rozkwitu",
|
||||
"customPrimaryColor": "Kolor podstawowy",
|
||||
"customSecondaryColor": "Kolor dodatkowy",
|
||||
"description": "Skonfiguruj wygląd wizualizatora dźwięku",
|
||||
"fadeWhenIdle": "Przyciemnij przy bezczynności",
|
||||
"fadeWhenIdle-description": "Wycisz wizualizator, gdy nie jest odtwarzany dźwięk",
|
||||
"innerDiameter": "Średnica wewnętrzna",
|
||||
"mode": {
|
||||
"all": "Wszystkie",
|
||||
"bars": "Paski",
|
||||
"barsRings": "Paski + Pierścienie",
|
||||
"rings": "Pierścienie",
|
||||
"wave": "Fala",
|
||||
"waveRings": "Fala + Pierścienie"
|
||||
},
|
||||
"ringOpacity": "Krycie pierścienia",
|
||||
"rotationSpeed": "Prędkość obrotu",
|
||||
"sensitivity": "Czułość",
|
||||
"title": "Ustawienia wizualizatora",
|
||||
"useCustomColors": "Użyj własnych kolorów",
|
||||
"useCustomColors-description": "Zastąp kolory motywu własnymi kolorami",
|
||||
"visualizationMode": "Tryb wizualizacji",
|
||||
"visualizationMode-description": "Wybierz styl wizualizacji",
|
||||
"waveThickness": "Grubość fali"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Largura da Barra",
|
||||
"bloomIntensity": "Intensidade do Bloom",
|
||||
"customPrimaryColor": "Cor primária",
|
||||
"customSecondaryColor": "Cor secundária",
|
||||
"description": "Configurar a aparência do visualizador de áudio",
|
||||
"fadeWhenIdle": "Desaparecer quando inativo",
|
||||
"fadeWhenIdle-description": "Desvanecer o visualizador quando nenhum áudio estiver sendo reproduzido.",
|
||||
"innerDiameter": "Diâmetro interno",
|
||||
"mode": {
|
||||
"all": "Tudo",
|
||||
"bars": "Barras",
|
||||
"barsRings": "Barras + Argolas",
|
||||
"rings": "Anéis",
|
||||
"wave": "Onda",
|
||||
"waveRings": "Onda + Anéis"
|
||||
},
|
||||
"ringOpacity": "Opacidade do Anel",
|
||||
"rotationSpeed": "Velocidade de rotação",
|
||||
"sensitivity": "Sensibilidade",
|
||||
"title": "Configurações do Visualizador",
|
||||
"useCustomColors": "Usar Cores Personalizadas",
|
||||
"useCustomColors-description": "Substituir as cores do tema por cores personalizadas",
|
||||
"visualizationMode": "Modo de Visualização",
|
||||
"visualizationMode-description": "Escolha o estilo de visualização",
|
||||
"waveThickness": "Espessura da onda"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ширина полосы",
|
||||
"bloomIntensity": "Интенсивность свечения",
|
||||
"customPrimaryColor": "Основной цвет",
|
||||
"customSecondaryColor": "Вторичный цвет",
|
||||
"description": "Настроить внешний вид визуализатора звука",
|
||||
"fadeWhenIdle": "Затухать в режиме ожидания",
|
||||
"fadeWhenIdle-description": "Затухать визуализатору при отсутствии воспроизведения аудио.",
|
||||
"innerDiameter": "Внутренний диаметр",
|
||||
"mode": {
|
||||
"all": "Всё",
|
||||
"bars": "Бары",
|
||||
"barsRings": "Турники + Кольца",
|
||||
"rings": "Кольца",
|
||||
"wave": "Волна",
|
||||
"waveRings": "Волна + Кольца"
|
||||
},
|
||||
"ringOpacity": "Прозрачность кольца",
|
||||
"rotationSpeed": "Скорость вращения",
|
||||
"sensitivity": "Чувствительность",
|
||||
"title": "Настройки визуализатора",
|
||||
"useCustomColors": "Использовать пользовательские цвета",
|
||||
"useCustomColors-description": "Переопределить цвета темы пользовательскими цветами",
|
||||
"visualizationMode": "Режим визуализации",
|
||||
"visualizationMode-description": "Выберите стиль визуализации",
|
||||
"waveThickness": "Толщина волны"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Çubuk Genişliği",
|
||||
"bloomIntensity": "Çiçeklenme Yoğunluğu",
|
||||
"customPrimaryColor": "Ana Renk",
|
||||
"customSecondaryColor": "İkincil Renk",
|
||||
"description": "Ses görselleştirici görünümünü yapılandır",
|
||||
"fadeWhenIdle": "Boşta Kalınca Karart",
|
||||
"fadeWhenIdle-description": "Ses çalmadığında görselleştiriciyi soldur.",
|
||||
"innerDiameter": "İç Çap",
|
||||
"mode": {
|
||||
"all": "Tüm",
|
||||
"bars": "Barlar",
|
||||
"barsRings": "Barfiks Demiri + Halkalar",
|
||||
"rings": "Yüzükler",
|
||||
"wave": "Dalga",
|
||||
"waveRings": "Dalga + Halkalar"
|
||||
},
|
||||
"ringOpacity": "Halka Opaklığı",
|
||||
"rotationSpeed": "Dönme Hızı",
|
||||
"sensitivity": "Hassasiyet",
|
||||
"title": "Görselleştirici Ayarları",
|
||||
"useCustomColors": "Özel Renkleri Kullan",
|
||||
"useCustomColors-description": "Tema renklerini özel renklerle geçersiz kıl",
|
||||
"visualizationMode": "Görselleştirme Modu",
|
||||
"visualizationMode-description": "Görselleştirme stilini seçin",
|
||||
"waveThickness": "Dalga Kalınlığı"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Ширина бруска",
|
||||
"bloomIntensity": "Інтенсивність світіння",
|
||||
"customPrimaryColor": "Основний колір",
|
||||
"customSecondaryColor": "Вторинний колір",
|
||||
"description": "Налаштуйте вигляд аудіовізуалізатора",
|
||||
"fadeWhenIdle": "Згасати в режимі очікування",
|
||||
"fadeWhenIdle-description": "Затемнювати візуалізатор, коли не відтворюється звук.",
|
||||
"innerDiameter": "Внутрішній діаметр",
|
||||
"mode": {
|
||||
"all": "Все",
|
||||
"bars": "Бари",
|
||||
"barsRings": "Бруси + Кільця",
|
||||
"rings": "Кільця",
|
||||
"wave": "Хвиля",
|
||||
"waveRings": "Хвиля + Кільця"
|
||||
},
|
||||
"ringOpacity": "Прозорість кільця",
|
||||
"rotationSpeed": "Швидкість обертання",
|
||||
"sensitivity": "Чутливість",
|
||||
"title": "Налаштування візуалізатора",
|
||||
"useCustomColors": "Використовувати власні кольори",
|
||||
"useCustomColors-description": "Перевизначити кольори теми власними кольорами",
|
||||
"visualizationMode": "Режим візуалізації",
|
||||
"visualizationMode-description": "Виберіть стиль візуалізації",
|
||||
"waveThickness": "Товщина хвилі"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "Độ rộng thanh",
|
||||
"bloomIntensity": "Cường độ hiệu ứng bloom",
|
||||
"customPrimaryColor": "Màu chính tùy chỉnh",
|
||||
"customSecondaryColor": "Màu phụ tùy chỉnh",
|
||||
"description": "Cấu hình giao diện trình hiển thị âm thanh",
|
||||
"fadeWhenIdle": "Làm mờ khi không hoạt động",
|
||||
"fadeWhenIdle-description": "Làm mờ trình hiển thị khi không có âm thanh",
|
||||
"innerDiameter": "Đường kính bên trong",
|
||||
"mode": {
|
||||
"all": "Tất cả",
|
||||
"bars": "Thanh",
|
||||
"barsRings": "Thanh + Vòng",
|
||||
"rings": "Vòng",
|
||||
"wave": "Sóng",
|
||||
"waveRings": "Sóng + Vòng"
|
||||
},
|
||||
"ringOpacity": "Độ mờ vòng",
|
||||
"rotationSpeed": "Tốc độ xoay",
|
||||
"sensitivity": "Độ nhạy",
|
||||
"title": "Cài đặt trình hiển thị",
|
||||
"useCustomColors": "Dùng màu tùy chỉnh",
|
||||
"useCustomColors-description": "Ghi đè màu giao diện bằng màu tùy chỉnh",
|
||||
"visualizationMode": "Chế độ hiển thị",
|
||||
"visualizationMode-description": "Chọn kiểu hiển thị",
|
||||
"waveThickness": "Độ dày sóng"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"settings": {
|
||||
"barWidth": "条宽",
|
||||
"bloomIntensity": "光晕强度",
|
||||
"customPrimaryColor": "原色",
|
||||
"customSecondaryColor": "间色",
|
||||
"description": "配置音频可视化效果外观",
|
||||
"fadeWhenIdle": "空闲时淡出",
|
||||
"fadeWhenIdle-description": "当没有音频播放时淡出可视化工具",
|
||||
"innerDiameter": "内径",
|
||||
"mode": {
|
||||
"all": "全部",
|
||||
"bars": "酒吧",
|
||||
"barsRings": "单杠 + 吊环",
|
||||
"rings": "戒指",
|
||||
"wave": "波浪",
|
||||
"waveRings": "波浪 + 环"
|
||||
},
|
||||
"ringOpacity": "环透明度",
|
||||
"rotationSpeed": "转速",
|
||||
"sensitivity": "敏感性",
|
||||
"title": "可视化设置",
|
||||
"useCustomColors": "使用自定义颜色",
|
||||
"useCustomColors-description": "使用自定义颜色覆盖主题颜色",
|
||||
"visualizationMode": "可视化模式",
|
||||
"visualizationMode-description": "选择可视化风格",
|
||||
"waveThickness": "波浪厚度"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "fancy-audiovisualizer",
|
||||
"name": "Fancy Audiovisualizer",
|
||||
"version": "1.1.2",
|
||||
"minNoctaliaVersion": "4.6.6",
|
||||
"author": "Noctalia Team",
|
||||
"official": true,
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Lemmy's fancy circular audio visualizer.",
|
||||
"tags": [
|
||||
"Desktop",
|
||||
"Audio"
|
||||
],
|
||||
"entryPoints": {
|
||||
"desktopWidget": "DesktopWidget.qml",
|
||||
"desktopWidgetSettings": "DesktopWidgetSettings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"sensitivity": 1.5,
|
||||
"rotationSpeed": 0.5,
|
||||
"barWidth": 0.6,
|
||||
"ringOpacity": 0.8,
|
||||
"bloomIntensity": 0.5,
|
||||
"visualizationMode": 3,
|
||||
"waveThickness": 1.0,
|
||||
"innerDiameter": 0.7,
|
||||
"fadeWhenIdle": false,
|
||||
"useCustomColors": false,
|
||||
"customPrimaryColor": "#6750A4",
|
||||
"customSecondaryColor": "#625B71"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 575 KiB |
@@ -0,0 +1,559 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float time;
|
||||
float itemWidth;
|
||||
float itemHeight;
|
||||
vec4 primaryColor;
|
||||
vec4 secondaryColor;
|
||||
float sensitivity;
|
||||
float rotationSpeed;
|
||||
float barWidth;
|
||||
float ringOpacity;
|
||||
float cornerRadius;
|
||||
float bloomIntensity;
|
||||
float visualizationMode; // 0=bars, 1=wave, 2=rings, 3=bars+rings, 4=wave+rings, 5=all
|
||||
float waveThickness;
|
||||
float innerDiameter;
|
||||
} ubuf;
|
||||
|
||||
// Mode helper functions
|
||||
bool hasRings() { return ubuf.visualizationMode >= 2.0; }
|
||||
bool hasBars() { return ubuf.visualizationMode == 0.0 || ubuf.visualizationMode == 3.0 || ubuf.visualizationMode >= 5.0; }
|
||||
bool hasWave() { return ubuf.visualizationMode == 1.0 || ubuf.visualizationMode == 4.0 || ubuf.visualizationMode >= 5.0; }
|
||||
|
||||
layout(binding = 1) uniform sampler2D source;
|
||||
|
||||
#define TWOPI 6.28318530718
|
||||
#define PI 3.14159265359
|
||||
#define NBARS 32
|
||||
|
||||
// Sample audio amplitude at normalized position (0.0-1.0)
|
||||
float getAudio(float pos) {
|
||||
return texture(source, vec2(clamp(pos, 0.0, 1.0), 0.5)).r;
|
||||
}
|
||||
|
||||
// Smoothed audio sampling with interpolation
|
||||
float smoothAudio(float pos) {
|
||||
float idx = pos * float(NBARS - 1);
|
||||
float frac = fract(idx);
|
||||
float i0 = floor(idx) / float(NBARS - 1);
|
||||
float i1 = ceil(idx) / float(NBARS - 1);
|
||||
return mix(getAudio(i0), getAudio(i1), frac) * ubuf.sensitivity;
|
||||
}
|
||||
|
||||
// Frequency band helpers
|
||||
float getBass() { return smoothAudio(0.05); }
|
||||
float getMid() { return smoothAudio(0.3); }
|
||||
float getHighMid() { return smoothAudio(0.6); }
|
||||
float getTreble() { return smoothAudio(0.9); }
|
||||
|
||||
// SDF for rounded rectangle
|
||||
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
|
||||
vec2 q = abs(center) - size + radius;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
||||
}
|
||||
|
||||
// Compute polar wave visualization
|
||||
vec4 computePolarWave(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
|
||||
float theta = atan(centered.y, centered.x);
|
||||
float d = length(centered);
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
|
||||
// RING SYSTEM
|
||||
if (hasRings()) {
|
||||
// Center Waves
|
||||
if (d < innerRadius * 0.6) {
|
||||
float wave = mid * 0.8;
|
||||
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
|
||||
if (ripple > 0.7) {
|
||||
float intensity = clamp(mid * 0.6, 0.0, 0.4);
|
||||
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
|
||||
color = max(color, waveColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Energy Ring
|
||||
float energyRad = innerRadius * 0.65;
|
||||
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
|
||||
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
|
||||
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
|
||||
if (mod(segmentAngle, 1.0) < 0.6) {
|
||||
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
|
||||
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
|
||||
color = max(color, energyColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Particle Ring
|
||||
float particleRad = innerRadius * 0.75;
|
||||
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
|
||||
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
|
||||
float particleSpacing = TWOPI / 16.0;
|
||||
if (mod(particleAngle, particleSpacing) < 0.15) {
|
||||
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
|
||||
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
|
||||
color = max(color, particleColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Tech Grid Ring
|
||||
float gridRad = innerRadius * 0.85;
|
||||
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
|
||||
float gridAngle = theta + iTime * ubuf.rotationSpeed;
|
||||
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
|
||||
if (mod(gridAngle, 0.2) < gridDensity) {
|
||||
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
|
||||
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
|
||||
color = max(color, gridColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Accent Ring
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float pulse = clamp(bass * 0.08, 0.0, 0.05);
|
||||
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
|
||||
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
|
||||
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
|
||||
ringColor.a = ubuf.ringOpacity;
|
||||
ringColor.rgb *= 1.0 + bass * 0.3;
|
||||
color = max(color, ringColor);
|
||||
}
|
||||
|
||||
// Outer Ring
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
|
||||
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
|
||||
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
|
||||
outerColor.rgb *= 1.0 + bass * 0.4;
|
||||
color = max(color, outerColor);
|
||||
}
|
||||
}
|
||||
|
||||
// POLAR WAVE - filled shape with mirrored bands (64 visual bands from 32 samples)
|
||||
if (hasWave()) {
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
|
||||
// Map angle to 0-1 range around the full circle
|
||||
float normalizedAngle = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
|
||||
// Mirror: first half (0-0.5) maps to bands 0->31, second half (0.5-1) maps back 31->0
|
||||
float mirroredPos = normalizedAngle < 0.5 ? normalizedAngle * 2.0 : (1.0 - normalizedAngle) * 2.0;
|
||||
|
||||
// Catmull-Rom spline interpolation for smooth curve through mirrored bands
|
||||
float bandPos = mirroredPos * float(NBARS - 1);
|
||||
float fband1 = floor(bandPos);
|
||||
float fband0 = max(fband1 - 1.0, 0.0);
|
||||
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
|
||||
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
|
||||
|
||||
float t = fract(bandPos);
|
||||
|
||||
// Sample the 4 control points
|
||||
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
|
||||
// Catmull-Rom spline interpolation
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
float smoothedAudio = 0.5 * (
|
||||
(2.0 * p1) +
|
||||
(-p0 + p2) * t +
|
||||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
||||
);
|
||||
smoothedAudio = max(smoothedAudio, 0.0);
|
||||
|
||||
// Calculate wave radius at this angle
|
||||
float waveDisplacement = smoothedAudio * 0.5;
|
||||
float waveRadius = baseRadius + waveDisplacement; // Fixed outer extent
|
||||
|
||||
// Fill the entire area from inner to wave edge
|
||||
if (d >= innerRadius && d <= waveRadius) {
|
||||
float fillFactor = (d - innerRadius) / max(waveRadius - innerRadius, 0.001);
|
||||
|
||||
// Gradient from primary at base to accent at edge
|
||||
vec3 fillColor = mix(ubuf.primaryColor.rgb * 0.8, ubuf.secondaryColor.rgb, fillFactor);
|
||||
|
||||
// Boost brightness with bass
|
||||
fillColor *= 1.0 + bass * 0.3;
|
||||
|
||||
// Alpha: stronger near the edge, fades toward center
|
||||
float fillAlpha = mix(0.4, 1.0, fillFactor) * ubuf.waveThickness;
|
||||
fillAlpha = clamp(fillAlpha, 0.0, 1.0);
|
||||
|
||||
vec4 fill = vec4(fillColor, fillAlpha);
|
||||
color = max(color, fill);
|
||||
}
|
||||
|
||||
// Bright edge line at the wave boundary
|
||||
float edgeThickness = ubuf.waveThickness * 0.025;
|
||||
float distToEdge = abs(d - waveRadius);
|
||||
if (distToEdge < edgeThickness) {
|
||||
float edgeFactor = 1.0 - smoothstep(0.0, edgeThickness, distToEdge);
|
||||
vec3 edgeColor = ubuf.secondaryColor.rgb * (1.2 + smoothedAudio * 0.5);
|
||||
|
||||
// Add highlight at peaks
|
||||
if (smoothedAudio > 0.5) {
|
||||
edgeColor += vec3(0.3, 0.4, 0.5) * (smoothedAudio - 0.5);
|
||||
}
|
||||
|
||||
vec4 edge = vec4(edgeColor, edgeFactor);
|
||||
color = max(color, edge);
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
// Compute visualization color at given UV coordinates
|
||||
// Returns vec4 with RGB color and alpha
|
||||
vec4 computeVisualization(vec2 uv, float iTime, float bass, float mid, float highMid, float treble) {
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
|
||||
float theta = atan(centered.y, centered.x);
|
||||
float d = length(centered);
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
|
||||
// RING SYSTEM
|
||||
if (hasRings()) {
|
||||
// Center Waves
|
||||
if (d < innerRadius * 0.6) {
|
||||
float wave = mid * 0.8;
|
||||
float ripple = sin(d * 25.0 + wave * 15.0 - iTime * 2.0);
|
||||
if (ripple > 0.7) {
|
||||
float intensity = clamp(mid * 0.6, 0.0, 0.4);
|
||||
vec4 waveColor = ubuf.secondaryColor * intensity * ubuf.ringOpacity;
|
||||
color = max(color, waveColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Energy Ring
|
||||
float energyRad = innerRadius * 0.65;
|
||||
float energyThickness = 0.015 + clamp(highMid * 0.02, 0.0, 0.03);
|
||||
if (d > energyRad - energyThickness && d < energyRad + energyThickness) {
|
||||
float segmentAngle = theta * 8.0 + highMid * 3.0 + iTime;
|
||||
if (mod(segmentAngle, 1.0) < 0.6) {
|
||||
float alpha = clamp(highMid * 2.0, 0.3, 1.0) * ubuf.ringOpacity;
|
||||
vec4 energyColor = mix(ubuf.primaryColor, ubuf.secondaryColor, 0.5) * alpha;
|
||||
color = max(color, energyColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Particle Ring
|
||||
float particleRad = innerRadius * 0.75;
|
||||
if (d > particleRad - 0.02 && d < particleRad + 0.02) {
|
||||
float particleAngle = theta + treble * 2.0 + iTime * 0.5;
|
||||
float particleSpacing = TWOPI / 16.0;
|
||||
if (mod(particleAngle, particleSpacing) < 0.15) {
|
||||
float brightness = 0.5 + clamp(treble * 1.5, 0.0, 0.5);
|
||||
vec4 particleColor = ubuf.secondaryColor * brightness * ubuf.ringOpacity;
|
||||
color = max(color, particleColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Tech Grid Ring
|
||||
float gridRad = innerRadius * 0.85;
|
||||
if (d > gridRad - 0.012 && d < gridRad + 0.012) {
|
||||
float gridAngle = theta + iTime * ubuf.rotationSpeed;
|
||||
float gridDensity = 0.08 + clamp(mid * 0.05, 0.0, 0.1);
|
||||
if (mod(gridAngle, 0.2) < gridDensity) {
|
||||
vec4 gridColor = ubuf.primaryColor * 0.7 * ubuf.ringOpacity;
|
||||
gridColor.rgb += vec3(0.1, 0.15, 0.2) * clamp(mid, 0.0, 0.8);
|
||||
color = max(color, gridColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Accent Ring
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float pulse = clamp(bass * 0.08, 0.0, 0.05);
|
||||
if (d > accentRad - pulse - 0.008 && d < accentRad + pulse + 0.015) {
|
||||
float colorShift = clamp(bass * 0.5, 0.0, 1.0);
|
||||
vec4 ringColor = mix(ubuf.secondaryColor * 0.7, ubuf.primaryColor, colorShift);
|
||||
ringColor.a = ubuf.ringOpacity;
|
||||
ringColor.rgb *= 1.0 + bass * 0.3;
|
||||
color = max(color, ringColor);
|
||||
}
|
||||
|
||||
// Outer Ring
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
if (d > outerRad - 0.008 && d < outerRad + 0.008) {
|
||||
vec4 outerColor = ubuf.primaryColor * ubuf.ringOpacity;
|
||||
outerColor.rgb += vec3(0.2, 0.3, 0.4) * clamp(treble * 0.5, 0.0, 0.3);
|
||||
outerColor.rgb *= 1.0 + bass * 0.4;
|
||||
color = max(color, outerColor);
|
||||
}
|
||||
}
|
||||
|
||||
// CIRCULAR AUDIO BARS (64 bars, mirrored from 32 audio samples)
|
||||
if (hasBars() && d > innerRadius) {
|
||||
// Double the visual bars by using NBARS * 2
|
||||
float section = TWOPI / float(NBARS * 2);
|
||||
float center = section / 2.0;
|
||||
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
float m = mod(adjustedTheta, section);
|
||||
float ym = d * sin(center - m);
|
||||
|
||||
float barW = ubuf.barWidth * 0.015;
|
||||
if (abs(ym) < barW) {
|
||||
// Calculate position in the circle (0.0 to 1.0)
|
||||
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
// Mirror: first half (0-0.5) maps to 0-1, second half (0.5-1) maps back 1-0
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float v = smoothAudio(mirroredPos);
|
||||
|
||||
float wave = sin(theta * 3.0 + mid * 5.0) * clamp(mid * 0.03, 0.0, 0.05);
|
||||
v += wave;
|
||||
v = max(v, 0.0);
|
||||
|
||||
float barStart = innerRadius;
|
||||
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
|
||||
|
||||
if (d >= barStart && d <= barEnd) {
|
||||
float heightFactor = (d - barStart) / max(barEnd - barStart, 0.001);
|
||||
|
||||
vec3 bottomColor = ubuf.primaryColor.rgb * 0.6;
|
||||
vec3 middleColor = ubuf.primaryColor.rgb;
|
||||
vec3 topColor = ubuf.secondaryColor.rgb;
|
||||
|
||||
vec3 barColor;
|
||||
if (heightFactor < 0.5) {
|
||||
barColor = mix(bottomColor, middleColor, heightFactor * 2.0);
|
||||
} else {
|
||||
barColor = mix(middleColor, topColor, (heightFactor - 0.5) * 2.0);
|
||||
}
|
||||
|
||||
barColor *= 1.0 + bass * 0.4;
|
||||
|
||||
if (heightFactor > 0.85) {
|
||||
barColor += vec3(0.3, 0.4, 0.5) * clamp(treble * 0.8, 0.0, 0.5);
|
||||
}
|
||||
|
||||
float edgeFactor = 1.0 - smoothstep(barW * 0.7, barW, abs(ym));
|
||||
vec4 finalBarColor = vec4(barColor, edgeFactor);
|
||||
color = max(color, finalBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
void main() {
|
||||
// ============================================
|
||||
// DYNAMIC CONTENT SCALING
|
||||
// ============================================
|
||||
// Calculate max possible extent from current settings to guarantee
|
||||
// nothing ever reaches the widget border.
|
||||
//
|
||||
// Max visualization radius in centered space [-1,1]:
|
||||
// Bars/Wave: baseRadius(0.35) + sensitivity * 0.5
|
||||
// Rings: innerDiameter/2 + sensitivity * 0.05 (always smaller)
|
||||
//
|
||||
// Bloom reach: exp(-dist * minDecayRate / bloomIntensity) decays to
|
||||
// < 1/255 at dist ≈ bloomIntensity * 1.0 (from solving exp decay
|
||||
// with worst-case amplitude * multiplier chain)
|
||||
//
|
||||
// contentScale maps the widget edge to ±contentScale in centered space,
|
||||
// so setting it to maxTotalRadius ensures everything fits.
|
||||
|
||||
float maxContentRadius = 0.35 + ubuf.sensitivity * 0.5;
|
||||
float maxBloomReach = ubuf.bloomIntensity * 1.0;
|
||||
float maxTotalRadius = maxContentRadius + maxBloomReach;
|
||||
float contentScale = max(maxTotalRadius * 1.05, 1.0); // 5% safety margin
|
||||
|
||||
vec2 uv = (qt_TexCoord0 - 0.5) * contentScale + 0.5;
|
||||
|
||||
// Convert linear time (0-3600) to smooth oscillation for seamless looping
|
||||
// sin() ensures perfect continuity when QML wraps from 3600 back to 0
|
||||
float iTime = sin(ubuf.time * TWOPI / 3600.0) * 1800.0 + 1800.0;
|
||||
|
||||
// Frequency analysis
|
||||
float bass = getBass();
|
||||
float mid = getMid();
|
||||
float highMid = getHighMid();
|
||||
float treble = getTreble();
|
||||
|
||||
// Get base visualization color based on mode
|
||||
// Mode 0: bars only, Mode 1: wave only, Mode 2: rings only
|
||||
// Mode 3: bars+rings, Mode 4: wave+rings, Mode 5: all
|
||||
vec4 color;
|
||||
if (hasWave() && !hasBars()) {
|
||||
// Wave only or wave+rings (modes 1, 4)
|
||||
color = computePolarWave(uv, iTime, bass, mid, highMid, treble);
|
||||
} else if (hasBars() && !hasWave()) {
|
||||
// Bars only or bars+rings (modes 0, 3)
|
||||
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
} else if (hasWave() && hasBars()) {
|
||||
// All mode (5) - combine both
|
||||
vec4 barsColor = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
vec4 waveColor = computePolarWave(uv, iTime, bass, mid, highMid, treble);
|
||||
color = max(barsColor, waveColor);
|
||||
} else {
|
||||
// Rings only (mode 2) - still need to call one of them for ring rendering
|
||||
color = computeVisualization(uv, iTime, bass, mid, highMid, treble);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// BLOOM EFFECT - Glow based on distance to geometry
|
||||
// ============================================
|
||||
|
||||
if (ubuf.bloomIntensity > 0.01 && color.a < 0.01) {
|
||||
// Only apply bloom where there's no geometry (empty space)
|
||||
// Find distance to nearest bright element
|
||||
|
||||
float aspect = ubuf.itemWidth / ubuf.itemHeight;
|
||||
vec2 centered = (uv - 0.5) * 2.0;
|
||||
centered.x *= aspect;
|
||||
float d = length(centered);
|
||||
float theta = atan(centered.y, centered.x);
|
||||
|
||||
float innerRadius = ubuf.innerDiameter / 2.0;
|
||||
float baseRadius = 0.35; // Fixed reference for outer extent
|
||||
float glowAmount = 0.0;
|
||||
vec3 glowColor = vec3(0.0);
|
||||
|
||||
// Glow from rings (if enabled)
|
||||
if (hasRings()) {
|
||||
// Outer ring glow
|
||||
float outerRad = innerRadius + bass * 0.05;
|
||||
float ringDist = abs(d - outerRad);
|
||||
float ringGlow = exp(-ringDist * 8.0 / ubuf.bloomIntensity) * (1.0 + bass * 0.5);
|
||||
glowColor += ubuf.primaryColor.rgb * ringGlow;
|
||||
glowAmount = max(glowAmount, ringGlow);
|
||||
|
||||
// Accent ring glow
|
||||
float accentRad = innerRadius * 0.92;
|
||||
float accentDist = abs(d - accentRad);
|
||||
float accentGlow = exp(-accentDist * 10.0 / ubuf.bloomIntensity) * (0.7 + bass * 0.3);
|
||||
glowColor += mix(ubuf.secondaryColor.rgb, ubuf.primaryColor.rgb, 0.5) * accentGlow;
|
||||
glowAmount = max(glowAmount, accentGlow);
|
||||
}
|
||||
|
||||
// Glow from visualization (bars or polar wave)
|
||||
if ((hasBars() || hasWave()) && d > innerRadius * 0.8) {
|
||||
float adjustedTheta = theta + PI + iTime * ubuf.rotationSpeed * 0.2;
|
||||
float circlePos = mod(adjustedTheta, TWOPI) / TWOPI;
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float v = smoothAudio(mirroredPos);
|
||||
|
||||
if (hasWave()) {
|
||||
// Polar wave bloom - Catmull-Rom spline with mirroring matching main render
|
||||
float mirroredPos = circlePos < 0.5 ? circlePos * 2.0 : (1.0 - circlePos) * 2.0;
|
||||
float bandPos = mirroredPos * float(NBARS - 1);
|
||||
float fband1 = floor(bandPos);
|
||||
float fband0 = max(fband1 - 1.0, 0.0);
|
||||
float fband2 = min(fband1 + 1.0, float(NBARS - 1));
|
||||
float fband3 = min(fband1 + 2.0, float(NBARS - 1));
|
||||
|
||||
float t = fract(bandPos);
|
||||
float p0 = getAudio(fband0 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p1 = getAudio(fband1 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p2 = getAudio(fband2 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
float p3 = getAudio(fband3 / float(NBARS - 1)) * ubuf.sensitivity;
|
||||
|
||||
float t2 = t * t;
|
||||
float t3 = t2 * t;
|
||||
float smoothedAudio = 0.5 * (
|
||||
(2.0 * p1) +
|
||||
(-p0 + p2) * t +
|
||||
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
||||
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
||||
);
|
||||
smoothedAudio = max(smoothedAudio, 0.0);
|
||||
|
||||
float waveRadius = baseRadius + smoothedAudio * 0.5;
|
||||
|
||||
// Glow from the filled area and edge
|
||||
float distToWave = abs(d - waveRadius);
|
||||
float waveGlow = exp(-distToWave * 8.0 / ubuf.bloomIntensity) * smoothedAudio * 2.5;
|
||||
|
||||
vec3 waveGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, smoothedAudio);
|
||||
glowColor += waveGlowColor * waveGlow;
|
||||
glowAmount = max(glowAmount, waveGlow);
|
||||
}
|
||||
|
||||
if (hasBars()) {
|
||||
// Bars bloom
|
||||
float section = TWOPI / float(NBARS * 2);
|
||||
float m = mod(adjustedTheta, section);
|
||||
float center = section / 2.0;
|
||||
|
||||
float barAngleDist = min(abs(m - center), section - abs(m - center));
|
||||
float barEnd = baseRadius + v * 0.5; // Fixed outer extent
|
||||
|
||||
float radialDist = 0.0;
|
||||
if (d < innerRadius) {
|
||||
radialDist = innerRadius - d;
|
||||
} else if (d > barEnd) {
|
||||
radialDist = d - barEnd;
|
||||
}
|
||||
|
||||
float totalDist = length(vec2(barAngleDist * d, radialDist));
|
||||
float barGlow = exp(-totalDist * 15.0 / ubuf.bloomIntensity) * v * 2.0;
|
||||
|
||||
float heightFactor = clamp((d - innerRadius) / max(barEnd - innerRadius, 0.001), 0.0, 1.0);
|
||||
vec3 barGlowColor = mix(ubuf.primaryColor.rgb, ubuf.secondaryColor.rgb, heightFactor);
|
||||
|
||||
glowColor += barGlowColor * barGlow;
|
||||
glowAmount = max(glowAmount, barGlow);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply bloom
|
||||
float bloomMult = ubuf.bloomIntensity * (1.0 + bass * 0.5);
|
||||
color.rgb = glowColor * bloomMult;
|
||||
color.a = glowAmount * bloomMult * 0.6;
|
||||
|
||||
// Clamp to reasonable values
|
||||
color.rgb = min(color.rgb, vec3(1.5));
|
||||
color.a = min(color.a, 0.8);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// EDGE FADE - radial falloff that only affects the bloom zone
|
||||
// ============================================
|
||||
// Fade starts just past where main content ends (maxContentRadius)
|
||||
// and reaches zero at the widget edge (contentScale) in centered space.
|
||||
// This catches bloom tails without affecting the main visualization.
|
||||
|
||||
vec2 fromCenter = (qt_TexCoord0 - 0.5) * 2.0; // -1 to 1 in widget space
|
||||
float edgeProximity = max(abs(fromCenter.x), abs(fromCenter.y)); // box distance
|
||||
float fadeStart = maxContentRadius / contentScale; // where content ends in widget space
|
||||
float edgeFade = 1.0 - smoothstep(fadeStart, 1.0, edgeProximity);
|
||||
color *= edgeFade;
|
||||
|
||||
// ============================================
|
||||
// CORNER MASKING
|
||||
// ============================================
|
||||
|
||||
vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight);
|
||||
vec2 centerPos = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
|
||||
vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5;
|
||||
float dist = roundedBoxSDF(centerPos, halfSize, ubuf.cornerRadius);
|
||||
float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist);
|
||||
|
||||
// Final output with premultiplied alpha
|
||||
float finalAlpha = color.a * ubuf.qt_Opacity * cornerMask;
|
||||
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property ShellScreen screen
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
readonly property var main: pluginApi?.mainInstance ?? ({})
|
||||
|
||||
readonly property string screenName: screen?.name ?? ""
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isBarVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName)
|
||||
readonly property string displayMode: root.pluginSettings.displayMode ?? "onhover"
|
||||
readonly property string connectedColor: root.pluginSettings.connectedColor
|
||||
readonly property string disconnectedColor: root.pluginSettings.disconnectedColor
|
||||
readonly property var vpnList: root.main.vpnList ?? []
|
||||
readonly property real connectedCount: root.main.connectedCount ?? 0
|
||||
readonly property bool isLoading: root.main.isLoading ?? false
|
||||
|
||||
implicitWidth: pill.width
|
||||
implicitHeight: pill.height
|
||||
Component.onCompleted: {
|
||||
Logger.i("NetworkManagerVPN", "Bar widget loaded");
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: contextMenu
|
||||
|
||||
model: [{
|
||||
"label": pluginApi?.tr("settings.pluginSettings"),
|
||||
"action": "plugin-settings",
|
||||
"icon": "settings"
|
||||
}]
|
||||
onTriggered: (action) => {
|
||||
contextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
if (action === "plugin-settings")
|
||||
BarService.openPluginSettings(screen, pluginApi.manifest);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BarPill {
|
||||
id: pill
|
||||
|
||||
screen: root.screen
|
||||
oppositeDirection: BarService.getPillDirection(root)
|
||||
autoHide: false
|
||||
text: root.connectedCount > 0 ? pluginApi?.tr("common.connected") : pluginApi?.tr("common.disconnected")
|
||||
icon: root.isLoading ? "reload" : root.connectedCount > 0 ? "shield-lock" : "shield"
|
||||
onClicked: {
|
||||
if (pluginApi)
|
||||
pluginApi.openPanel(root.screen, root);
|
||||
|
||||
}
|
||||
onRightClicked: {
|
||||
if (pluginApi)
|
||||
pluginApi.closePanel(root.screen);
|
||||
PanelService.showContextMenu(contextMenu, pill, screen);
|
||||
}
|
||||
customIconColor: Color.resolveColorKeyOptional(root.connectedCount > 0 ? root.connectedColor : root.disconnectedColor)
|
||||
customTextColor: Color.resolveColorKeyOptional(root.connectedCount > 0 ? root.connectedColor : root.disconnectedColor)
|
||||
forceOpen: root.displayMode === "alwaysShow"
|
||||
forceClose: root.displayMode === "alwaysHide"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Widgets
|
||||
import qs.Commons
|
||||
|
||||
NIconButtonHot {
|
||||
property ShellScreen screen
|
||||
property var pluginApi: null
|
||||
|
||||
readonly property var main: pluginApi?.mainInstance ?? ({})
|
||||
readonly property real connectedCount: main.connectedCount ?? 0
|
||||
readonly property bool isLoading: main.isLoading ?? false
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
readonly property string connectedColor: pluginSettings.connectedColor
|
||||
readonly property string disconnectedColor: pluginSettings.disconnectedColor
|
||||
|
||||
icon: isLoading ? "reload" : connectedCount > 0 ? "shield-lock" : "shield"
|
||||
tooltipText: connectedCount > 0
|
||||
? pluginApi?.tr("common.connected")
|
||||
: pluginApi?.tr("common.disconnected")
|
||||
|
||||
colorFg: {
|
||||
const key = connectedCount > 0 ? connectedColor : disconnectedColor;
|
||||
if (!key || key === "none") return Color.mPrimary;
|
||||
return Color.resolveColorKeyOptional(key) ?? Color.mPrimary;
|
||||
}
|
||||
|
||||
onClicked: pluginApi?.togglePanel(screen, this)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? ({})
|
||||
|
||||
readonly property var toast: root.pluginSettings?.disableToastNotifications ? null : ToastService
|
||||
|
||||
property var pluginApi: null
|
||||
|
||||
property var vpnList: []
|
||||
property real connectedCount: 0
|
||||
readonly property bool isLoading: Object.keys(root._pending).length > 0
|
||||
|
||||
property var _pending: ({})
|
||||
|
||||
// Needed only to detect disconnection not initiated by the user
|
||||
property var _pollTimer: Timer {
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
property var _lines: []
|
||||
|
||||
property var _listProc: Process {
|
||||
command: ["nmcli", "-t", "-f", "NAME,TYPE,STATE,UUID", "connection", "show"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: (line) => {
|
||||
if (line.trim() !== "")
|
||||
root._lines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
const parsed = []
|
||||
const newPending = Object.assign({}, root._pending)
|
||||
for (const line of root._lines) {
|
||||
const parts = line.split(":")
|
||||
if (parts.length >= 4) {
|
||||
const name = parts[0]
|
||||
const type = parts[1]
|
||||
const state = parts[2]
|
||||
const uuid = parts[3]
|
||||
if (type === "vpn" || type === "wireguard") {
|
||||
if (newPending[uuid]) {
|
||||
const wasConnecting = newPending[uuid] === "connect"
|
||||
if (wasConnecting && state === "activated")
|
||||
delete newPending[uuid]
|
||||
else if (!wasConnecting && state !== "activated")
|
||||
delete newPending[uuid]
|
||||
}
|
||||
parsed.push({
|
||||
name,
|
||||
type,
|
||||
connected: state === "activated",
|
||||
isLoading: !!newPending[uuid],
|
||||
uuid
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
root._pending = newPending
|
||||
root.vpnList = parsed
|
||||
root.connectedCount = parsed.filter(v => v.connected).length
|
||||
}
|
||||
root._lines = []
|
||||
}
|
||||
}
|
||||
|
||||
property var _connectProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
command: ["nmcli", "connection", "up", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.connectedTo", { name: targetName }))
|
||||
else {
|
||||
root.stopLoading(targetUuid)
|
||||
toast?.showError(t("toast.connectionError", { name: targetName }))
|
||||
}
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
property var _disconnectProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
command: ["nmcli", "connection", "down", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.disconnectedFrom", { name: targetName }))
|
||||
else {
|
||||
root.stopLoading(targetUuid)
|
||||
toast?.showError(t("toast.disconnectionError", { name: targetName }))
|
||||
}
|
||||
root.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
property var _addProc: Process {
|
||||
property string targetType: ""
|
||||
|
||||
command: ["nm-connection-editor", "--create", "--type", targetType]
|
||||
onExited: (exitCode) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
property var _editProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
|
||||
command: ["nm-connection-editor", "--edit", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
property var _removeProc: Process {
|
||||
property string targetName: ""
|
||||
property string targetUuid: ""
|
||||
|
||||
command: ["nmcli", "connection", "delete", "uuid", targetUuid]
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
toast?.showNotice(t("toast.vpnRemoved", { "name": targetName }));
|
||||
else
|
||||
toast?.showError(t("toast.vpnRemoveError", { "name": targetName }));
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function t(key: string, data) {
|
||||
if (!pluginApi)
|
||||
return null;
|
||||
|
||||
return pluginApi.tr(key, data);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
_listProc.running = true
|
||||
}
|
||||
|
||||
function stopLoading(uuid) {
|
||||
if (uuid && _pending[uuid]) {
|
||||
const p = Object.assign({}, _pending)
|
||||
delete p[uuid]
|
||||
_pending = p
|
||||
}
|
||||
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
return Object.assign({}, v, { isLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
function connectTo(uuid) {
|
||||
const p = Object.assign({}, _pending)
|
||||
p[uuid] = "connect"
|
||||
_pending = p
|
||||
let name = ""
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
name = v.name
|
||||
return Object.assign({}, v, { isLoading: true })
|
||||
})
|
||||
_connectProc.targetName = name
|
||||
_connectProc.targetUuid = uuid
|
||||
_connectProc.running = true
|
||||
}
|
||||
|
||||
function disconnectFrom(uuid) {
|
||||
const p = Object.assign({}, _pending)
|
||||
p[uuid] = "disconnect"
|
||||
_pending = p
|
||||
let name = ""
|
||||
vpnList = vpnList.map(v => {
|
||||
if (v.uuid !== uuid) {
|
||||
return v
|
||||
}
|
||||
|
||||
name = v.name
|
||||
return Object.assign({}, v, { isLoading: true })
|
||||
})
|
||||
_disconnectProc.targetName = name
|
||||
_disconnectProc.targetUuid = uuid
|
||||
_disconnectProc.running = true
|
||||
}
|
||||
|
||||
function addConnection(type) {
|
||||
_addProc.targetType = type || "vpn";
|
||||
_addProc.running = true;
|
||||
}
|
||||
|
||||
function editConnection(uuid) {
|
||||
const vpn = vpnList.find((v) => {
|
||||
return v.uuid === uuid;
|
||||
});
|
||||
_editProc.targetName = vpn ? vpn.name : uuid;
|
||||
_editProc.targetUuid = uuid;
|
||||
_editProc.running = true;
|
||||
}
|
||||
|
||||
function removeConnection(uuid) {
|
||||
const vpn = vpnList.find((v) => {
|
||||
return v.uuid === uuid;
|
||||
});
|
||||
_removeProc.targetName = vpn ? vpn.name : uuid;
|
||||
_removeProc.targetUuid = uuid;
|
||||
_removeProc.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("NetworkManagerVPN", "Started")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
property ShellScreen screen
|
||||
readonly property var geometryPlaceholder: panelContainer
|
||||
readonly property bool allowAttach: true
|
||||
readonly property var main: pluginApi?.mainInstance ?? null
|
||||
readonly property var vpnList: main?.vpnList ?? []
|
||||
readonly property var activeList: vpnList.filter(v => v.connected || v.isLoading)
|
||||
readonly property var inactiveList: vpnList.filter(v => !v.connected && !v.isLoading)
|
||||
property real contentPreferredWidth: Math.round(500 * Style.uiScaleRatio)
|
||||
property real contentPreferredHeight: Math.min(500, mainColumn.implicitHeight + Style.marginL * 2)
|
||||
|
||||
Component.onCompleted: {
|
||||
if (main) main.refresh()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: panelContainer
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
|
||||
ColumnLayout {
|
||||
id: mainColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginM
|
||||
|
||||
// HEADER
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(header.implicitHeight + Style.marginM * 2 + 1)
|
||||
|
||||
ColumnLayout {
|
||||
id: header
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
NIcon {
|
||||
icon: "shield"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Color.mPrimary
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.vpn")
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "refresh"
|
||||
tooltipText: pluginApi?.tr("common.refresh")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
enabled: true
|
||||
onClicked: {
|
||||
if (main) {
|
||||
main.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: pluginApi?.tr("common.close")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: pluginApi.closePanel(pluginApi.panelOpenScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NScrollView {
|
||||
id: scrollView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
horizontalPolicy: ScrollBar.AlwaysOff
|
||||
verticalPolicy: ScrollBar.AsNeeded
|
||||
reserveScrollbarSpace: false
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
// CONNECTED
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(networksListActive.implicitHeight + Style.marginXL)
|
||||
visible: activeList.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: networksListActive
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.connected")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: activeList
|
||||
|
||||
VpnListItem {
|
||||
name: modelData.name
|
||||
type: modelData.type
|
||||
isConnected: true
|
||||
isLoading: modelData.isLoading
|
||||
onButtonClicked: {
|
||||
if (!main) return
|
||||
main.disconnectFrom(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DISCONNECTED
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(networksListInactive.implicitHeight + Style.marginXL)
|
||||
visible: inactiveList.length > 0
|
||||
|
||||
ColumnLayout {
|
||||
id: networksListInactive
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("common.disconnected")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: inactiveList
|
||||
|
||||
VpnListItem {
|
||||
name: modelData.name
|
||||
type: modelData.type
|
||||
isConnected: false
|
||||
isLoading: modelData.isLoading
|
||||
onButtonClicked: {
|
||||
if (!main) return
|
||||
main.connectTo(modelData.uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EMPTY
|
||||
NBox {
|
||||
id: emptyBox
|
||||
visible: vpnList.length < 1
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.round(emptyColumn.implicitHeight + Style.marginM * 2 + 1)
|
||||
|
||||
ColumnLayout {
|
||||
id: emptyColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
NIcon {
|
||||
icon: "search"
|
||||
pointSize: 48
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.emptyTitle")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NText {
|
||||
text: pluginApi?.tr("panel.emptyDescription")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("common.refresh")
|
||||
icon: "refresh"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
if (main) {
|
||||
main.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Network Manager VPN
|
||||
|
||||

|
||||
|
||||
Plugin to connect to your VPN connections from the bar and Control Center. Supports OpenVPN and WireGuard connections managed by NetworkManager. Editing and creation are achieved using
|
||||
nm-connection-editor
|
||||
|
||||
## Usage
|
||||
|
||||
Install the plugin and add it to your bar:
|
||||
|
||||
- **Bar widget** — A shield icon that shows if there is an active connection. Click it to open the VPN panel.
|
||||
- **VPN panel** — Lists all configured VPN connections.
|
||||
|
||||
The panel refreshes automatically every 5 seconds. You can also trigger a manual refresh with the refresh button.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Noctalia Shell** ≥ 3.6.0
|
||||
- **NetworkManager** with `nmcli`
|
||||
- **nm-connection-editor** to edit and create connections
|
||||
@@ -0,0 +1,256 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property var pluginApi: null
|
||||
readonly property var pluginSettings: pluginApi?.pluginSettings ?? pluginApi?.manifest?.metadata?.defaultSettings ?? ({})
|
||||
|
||||
readonly property var main: pluginApi?.mainInstance ?? null
|
||||
readonly property var vpnList: main ? main.vpnList : []
|
||||
// Local state
|
||||
property string editDisplayMode: root.pluginSettings.displayMode ?? ""
|
||||
property string editConnectedColor: root.pluginSettings.connectedColor ?? ""
|
||||
property string editDisconnectedColor: root.pluginSettings.disconnectedColor ?? ""
|
||||
property bool disableToastNotifications: root.pluginSettings?.disableToastNotifications ?? false
|
||||
|
||||
property string pendingDeleteUuid: ""
|
||||
property string pendingDeleteName: ""
|
||||
|
||||
readonly property var displayModeModel: [{
|
||||
"key": "onhover",
|
||||
"name": I18n.tr("display-modes.on-hover")
|
||||
}, {
|
||||
"key": "alwaysShow",
|
||||
"name": I18n.tr("display-modes.always-show")
|
||||
}, {
|
||||
"key": "alwaysHide",
|
||||
"name": I18n.tr("display-modes.always-hide")
|
||||
}]
|
||||
|
||||
function saveSettings() {
|
||||
pluginApi.pluginSettings.displayMode = root.editDisplayMode;
|
||||
pluginApi.pluginSettings.connectedColor = root.editConnectedColor;
|
||||
pluginApi.pluginSettings.disconnectedColor = root.editDisconnectedColor;
|
||||
pluginApi.pluginSettings.disableToastNotifications = root.disableToastNotifications;
|
||||
pluginApi.saveSettings();
|
||||
|
||||
Logger.i("NetworkManagerVpn", "Settings saved successfully");
|
||||
}
|
||||
|
||||
NComboBox {
|
||||
label: I18n.tr("common.display-mode")
|
||||
description: I18n.tr("bar.volume.display-mode-description")
|
||||
minimumWidth: 200
|
||||
model: root.displayModeModel
|
||||
currentKey: root.editDisplayMode
|
||||
onSelected: (key) => {
|
||||
root.editDisplayMode = key;
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: pluginApi?.tr("settings.connectedColor")
|
||||
description: pluginApi?.tr("settings.connectedColorDescription")
|
||||
currentKey: root.editConnectedColor
|
||||
onSelected: (key) => {
|
||||
root.editConnectedColor = key;
|
||||
}
|
||||
}
|
||||
|
||||
NColorChoice {
|
||||
label: pluginApi?.tr("settings.disconnectedColor")
|
||||
description: pluginApi?.tr("settings.disconnectedColorDescription")
|
||||
currentKey: root.editDisconnectedColor
|
||||
onSelected: (key) => {
|
||||
root.editDisconnectedColor = key;
|
||||
}
|
||||
}
|
||||
|
||||
NToggle {
|
||||
Layout.fillWidth: true
|
||||
label: pluginApi?.tr("settings.disableToastNotifications")
|
||||
description: pluginApi?.tr("settings.disableToastNotificationsDescription")
|
||||
checked: root.disableToastNotifications
|
||||
onToggled: checked => {
|
||||
root.disableToastNotifications = checked;
|
||||
}
|
||||
}
|
||||
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginM
|
||||
Layout.preferredHeight: Math.round(vpnSection.implicitHeight + Style.marginL * 2)
|
||||
|
||||
ColumnLayout {
|
||||
id: vpnSection
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginM
|
||||
|
||||
NLabel {
|
||||
label: pluginApi?.tr("settings.vpnConnections")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: root.vpnList.length === 0
|
||||
text: pluginApi?.tr("settings.noVpnConnections")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
Layout.leftMargin: Style.marginXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.vpnList
|
||||
|
||||
NBox {
|
||||
id: vpnRow
|
||||
|
||||
readonly property bool confirmingDelete: root.pendingDeleteUuid === modelData.uuid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.rightMargin: Style.marginXS
|
||||
implicitHeight: Math.round(rowContent.implicitHeight + Style.marginL)
|
||||
color: vpnRow.confirmingDelete ? Qt.alpha(Color.mError, 0.12) : Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: rowContent
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM
|
||||
anchors.rightMargin: Style.marginM
|
||||
anchors.topMargin: Style.marginS
|
||||
anchors.bottomMargin: Style.marginS
|
||||
spacing: Style.marginS
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "router"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: modelData.connected ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
NText {
|
||||
text: modelData.name
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: modelData.type || "vpn"
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !vpnRow.confirmingDelete
|
||||
icon: "edit"
|
||||
tooltipText: pluginApi?.tr("common.edit")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
onClicked: {
|
||||
main.editConnection(modelData.uuid);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
visible: !vpnRow.confirmingDelete
|
||||
icon: "trash-x"
|
||||
tooltipText: pluginApi?.tr("common.delete")
|
||||
baseSize: Style.baseWidgetSize * 0.75
|
||||
onClicked: {
|
||||
root.pendingDeleteUuid = modelData.uuid;
|
||||
root.pendingDeleteName = modelData.name;
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: vpnRow.confirmingDelete
|
||||
text: pluginApi?.tr("common.delete")
|
||||
fontSize: Style.fontSizeXS
|
||||
backgroundColor: Color.mError
|
||||
outlined: false
|
||||
onClicked: {
|
||||
main.removeConnection(root.pendingDeleteUuid);
|
||||
root.pendingDeleteUuid = "";
|
||||
root.pendingDeleteName = "";
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: vpnRow.confirmingDelete
|
||||
text: pluginApi?.tr("common.cancel")
|
||||
fontSize: Style.fontSizeXS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
root.pendingDeleteUuid = "";
|
||||
root.pendingDeleteName = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginXS
|
||||
spacing: Style.marginS
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("settings.addVpn")
|
||||
icon: "add"
|
||||
fontSize: Style.fontSizeS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
main.addConnection("vpn");
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi?.tr("settings.addWireguard")
|
||||
icon: "add"
|
||||
fontSize: Style.fontSizeS
|
||||
outlined: true
|
||||
onClicked: {
|
||||
main.addConnection("wireguard");
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
NBox {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property string type: ""
|
||||
property bool isConnected: false
|
||||
property bool isLoading: false
|
||||
|
||||
signal buttonClicked
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Style.marginXS
|
||||
Layout.rightMargin: Style.marginXS
|
||||
implicitHeight: Math.round(netColumn.implicitHeight + (Style.marginXL))
|
||||
|
||||
color: root.isConnected ? Qt.alpha(Color.mPrimary, 0.15) : Color.mSurface
|
||||
|
||||
ColumnLayout {
|
||||
id: netColumn
|
||||
width: parent.width - (Style.marginXL)
|
||||
x: Style.marginM
|
||||
y: Style.marginM
|
||||
spacing: Style.marginS
|
||||
|
||||
// Main row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginS
|
||||
|
||||
NIcon {
|
||||
icon: "router"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: root.isConnected ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
|
||||
NText {
|
||||
text: root.name
|
||||
pointSize: Style.fontSizeM
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: root.type
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: Style.marginXXS
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
visible: root.isConnected
|
||||
color: Color.mPrimary
|
||||
radius: height * 0.5
|
||||
width: Math.round(connectedText.implicitWidth + (Style.marginS * 2))
|
||||
height: Math.round(connectedText.implicitHeight + (Style.marginXS))
|
||||
|
||||
NText {
|
||||
id: connectedText
|
||||
anchors.centerIn: parent
|
||||
text: pluginApi?.tr("common.connected")
|
||||
pointSize: Style.fontSizeXXS
|
||||
color: Color.mOnPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action area
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
NBusyIndicator {
|
||||
visible: root.isLoading
|
||||
running: visible
|
||||
color: Color.mPrimary
|
||||
size: Style.baseWidgetSize * 0.5
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: root.isConnected
|
||||
text: pluginApi?.tr("common.disconnect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeS
|
||||
backgroundColor: Color.mError
|
||||
enabled: !root.isLoading
|
||||
onClicked: {
|
||||
root.buttonClicked()
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
visible: !root.isConnected
|
||||
text: pluginApi?.tr("common.connect")
|
||||
outlined: !hovered
|
||||
fontSize: Style.fontSizeS
|
||||
enabled: !root.isLoading
|
||||
onClicked: {
|
||||
root.buttonClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "Kein VPN gefunden",
|
||||
"emptyDescription": "Verwende den Netzwerk-Manager um ein VPN hinzuzufügen"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Verbunden mit {name}",
|
||||
"disconnectedFrom": "Von {Name} getrennt",
|
||||
"connectionError": "Verbindungsaufbau mit {name} fehlgeschlagen",
|
||||
"disconnectionError": "Die Trennung von {name} ist fehlgeschlagen",
|
||||
"vpnRemoved": "VPN \"{name}\" wurde entfernt",
|
||||
"vpnRemoveError": "Der VPN \"{name}\" konnte nicht entfernt werden"
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Plugin Einstellungen",
|
||||
"connectedColor": "Verknüpfte Farbe",
|
||||
"connectedColorDescription": "Wähle die Farbe für das Symbol und den Text aus, die bei bestehender Verbindung angezeigt werden sollen",
|
||||
"disconnectedColor": "Nicht verknüpfte Farbe",
|
||||
"disconnectedColorDescription": "Wähle die Farbe für das Symbol und den Text bei unterbrochener Verbindung aus",
|
||||
"vpnConnections": "VPN Verbindungen",
|
||||
"noVpnConnections": "Keine VPN Verbindungen konfiguriert",
|
||||
"addVpn": "VPN hinzufügen",
|
||||
"addWireguard": "WireGuard hinzufügen",
|
||||
"disableToastNotifications": "Toast-Benachrichtigungen deaktivieren",
|
||||
"disableToastNotificationsDescription": "Keine Toast-Benachrichtigungen anzeigen, wenn eine VPN-Verbindung hergestellt oder getrennt wird"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Aktualisieren",
|
||||
"connected": "Verbunden",
|
||||
"disconnected": "Getrennt",
|
||||
"connect": "Verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"close": "Schließen",
|
||||
"settings": "Einstellungen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "No VPN found",
|
||||
"emptyDescription": "Use Network Manager to add a VPN"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Connected to {name}",
|
||||
"disconnectedFrom": "Disconnected from {name}",
|
||||
"connectionError": "Failed to connect {name}",
|
||||
"disconnectionError": "Failed to disconnect {name}",
|
||||
"vpnRemoved": "VPN \"{name}\" removed",
|
||||
"vpnRemoveError": "Failed to remove VPN \"{name}\""
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Plugin settings",
|
||||
"connectedColor": "Connected color",
|
||||
"connectedColorDescription": "Chose the color to use for icon and text when connected",
|
||||
"disconnectedColor": "Disconnected color",
|
||||
"disconnectedColorDescription": "Chose the color to use for icon and text when disconnected",
|
||||
"vpnConnections": "VPN Connections",
|
||||
"noVpnConnections": "No VPN connections configured.",
|
||||
"addVpn": "Add VPN",
|
||||
"addWireguard": "Add WireGuard",
|
||||
"disableToastNotifications": "Disable toast notifications",
|
||||
"disableToastNotificationsDescription": "Do not show toast notifications when a VPN connects or disconnects"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Refresh",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"close": "Close",
|
||||
"settings": "Settings",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"panel": {
|
||||
"emptyTitle": "VPN не найден",
|
||||
"emptyDescription": "Используйте Менеджер Подключений для добавления VPN"
|
||||
},
|
||||
"toast": {
|
||||
"connectedTo": "Подключиться к {name}",
|
||||
"disconnectedFrom": "Отключиться от {name}",
|
||||
"connectionError": "Ошибка подключения к {name}",
|
||||
"disconnectionError": "Ошибка отключения от {name}",
|
||||
"vpnRemoved": "VPN \"{name}\" удалён",
|
||||
"vpnRemoveError": "Ошибка удаления VPN \"{name}\""
|
||||
},
|
||||
"settings": {
|
||||
"pluginSettings": "Настройка плагина",
|
||||
"connectedColor": "Цвет активного подключения",
|
||||
"connectedColorDescription": "Выберите цвет значка и текста при активном подключении",
|
||||
"disconnectedColor": "Цвет отсутствия подключения",
|
||||
"disconnectedColorDescription": "Выберите цвет значка и текста при отсутствии подключения",
|
||||
"vpnConnections": "VPN-соединения",
|
||||
"noVpnConnections": "VPN-подключения не настроены.",
|
||||
"addVpn": "Добавить VPN",
|
||||
"addWireguard": "Добавить WireGuard",
|
||||
"disableToastNotifications": "Отключить уведомления",
|
||||
"disableToastNotificationsDescription": "Не показывать уведомления о статусе VPN-соединения"
|
||||
},
|
||||
"common": {
|
||||
"vpn": "VPN",
|
||||
"refresh": "Обновить",
|
||||
"connected": "Подключено",
|
||||
"disconnected": "Отключено",
|
||||
"connect": "Подключить",
|
||||
"disconnect": "Отключить",
|
||||
"close": "Закрыть",
|
||||
"settings": "Настройки",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "network-manager-vpn",
|
||||
"name": "Network Manager VPN",
|
||||
"version": "1.3.0",
|
||||
"minNoctaliaVersion": "3.6.0",
|
||||
"author": "nZO",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/noctalia-dev/noctalia-plugins",
|
||||
"description": "Connect to VPN via NetworkManager",
|
||||
"tags": [
|
||||
"Bar",
|
||||
"Panel",
|
||||
"Network"
|
||||
],
|
||||
"entryPoints": {
|
||||
"main": "Main.qml",
|
||||
"barWidget": "BarWidget.qml",
|
||||
"controlCenterWidget": "ControlCenterWidget.qml",
|
||||
"panel": "Panel.qml",
|
||||
"settings": "Settings.qml"
|
||||
},
|
||||
"dependencies": {
|
||||
"plugins": []
|
||||
},
|
||||
"metadata": {
|
||||
"defaultSettings": {
|
||||
"displayMode": "onhover",
|
||||
"disconnectedColor": "none",
|
||||
"connectedColor": "primary",
|
||||
"disableToastNotifications": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 421 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"displayMode": "onhover",
|
||||
"disconnectedColor": "none",
|
||||
"connectedColor": "primary",
|
||||
"disableToastNotifications": false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Polkit
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var pluginApi: null
|
||||
|
||||
PolkitAgent {
|
||||
id: agent
|
||||
|
||||
onIsActiveChanged: {
|
||||
if (isActive) {
|
||||
openWindow()
|
||||
} else {
|
||||
closeWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var window: null
|
||||
|
||||
function openWindow() {
|
||||
if (agent.flow === null) {
|
||||
Logger.w("polkit-agent: Cannot open window, agent.flow is null");
|
||||
return;
|
||||
}
|
||||
if (window === null) {
|
||||
var component = Qt.createComponent("PolkitWindow.qml");
|
||||
if (component.status === Component.Ready) {
|
||||
window = component.createObject(root, {
|
||||
flow: agent.flow,
|
||||
pluginApi: root.pluginApi
|
||||
});
|
||||
if (window !== null) {
|
||||
window.visible = true;
|
||||
}
|
||||
}
|
||||
component.destroy();
|
||||
} else {
|
||||
window.flow = agent.flow;
|
||||
window.pluginApi = root.pluginApi;
|
||||
window.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeWindow() {
|
||||
if (window !== null) {
|
||||
window.destroy();
|
||||
window = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Polkit
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Widgets
|
||||
import qs.Services.UI
|
||||
|
||||
PanelWindow {
|
||||
id: polkitWindow
|
||||
|
||||
property AuthFlow flow
|
||||
property var pluginApi
|
||||
property string resolvedMessage: ""
|
||||
property var transientMatch: null
|
||||
|
||||
Connections {
|
||||
target: flow
|
||||
function onFailedChanged() {
|
||||
if (flow && flow.failed) {
|
||||
ToastService.showError(
|
||||
pluginApi ? pluginApi.tr("error.failed.title") : "Authentication Failed",
|
||||
pluginApi ? pluginApi.tr("error.failed.message") : "The password you entered was incorrect."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve transient service names when flow changes
|
||||
onFlowChanged: {
|
||||
resolveTransientServiceName(flow.message);
|
||||
}
|
||||
|
||||
// Resolve transient service names (run-PID-random.service) to actual command
|
||||
Process {
|
||||
id: cmdLineProcess
|
||||
command: ["cat", "/proc/0/cmdline"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: function() {
|
||||
var args = this.text.split(String.fromCharCode(0)).filter(function(s) { return s.length > 0; });
|
||||
Logger.i("polkit-agent: Process output length=" + this.text.length + ", args=" + args.length);
|
||||
if (args.length > 0 && polkitWindow.transientMatch) {
|
||||
var resolvedCmd = args.join(' ');
|
||||
var isCommand = args.length > 1 || args[0].includes('/');
|
||||
polkitWindow.resolvedMessage = polkitWindow.resolvedMessage.replace(polkitWindow.transientMatch[0], resolvedCmd);
|
||||
if (isCommand) {
|
||||
polkitWindow.resolvedMessage = polkitWindow.resolvedMessage.replace(/transient unit/i, 'run command');
|
||||
}
|
||||
Logger.i("polkit-agent: Resolved message: " + polkitWindow.resolvedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTransientServiceName(message) {
|
||||
if (!message) return;
|
||||
var match = message.match(/run-p?(\d+)-[^.]+\.service/);
|
||||
if (!match) {
|
||||
Logger.i("polkit-agent: No transient service pattern in message: " + message);
|
||||
polkitWindow.resolvedMessage = message;
|
||||
polkitWindow.transientMatch = null;
|
||||
return;
|
||||
}
|
||||
var pid = match[1];
|
||||
Logger.i("polkit-agent: Found transient service pattern, PID=" + pid + ", message=" + message);
|
||||
polkitWindow.resolvedMessage = message;
|
||||
polkitWindow.transientMatch = match;
|
||||
cmdLineProcess.command = ["cat", "/proc/" + pid + "/cmdline"];
|
||||
cmdLineProcess.running = true;
|
||||
}
|
||||
|
||||
// Layer above everything else (critical system prompt)
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
readonly property real shadowPadding: Style.shadowBlurMax + Style.marginL
|
||||
|
||||
// Explicit size - include shadowPadding so the shadow isn't clipped at window corners
|
||||
implicitWidth: 400 * Style.uiScaleRatio + shadowPadding * 2
|
||||
implicitHeight: contentLayout.implicitHeight + (Style.marginL * 2) + shadowPadding * 2
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: shadowPadding
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (!flow) return;
|
||||
|
||||
if (Keybinds.checkKey(event, "escape", Settings)) {
|
||||
flow.cancelAuthenticationRequest();
|
||||
event.accepted = true;
|
||||
} else if (Keybinds.checkKey(event, "enter", Settings)) {
|
||||
if (passwordInput.text !== "") {
|
||||
flow.submit(passwordInput.text);
|
||||
passwordInput.text = "";
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transform: Translate {
|
||||
id: shakeTranslate
|
||||
x: 0
|
||||
}
|
||||
|
||||
// Error animation
|
||||
SequentialAnimation {
|
||||
id: errorShake
|
||||
running: flow && flow.failed
|
||||
loops: 1
|
||||
|
||||
NumberAnimation { target: shakeTranslate; property: "x"; from: 0; to: -10; duration: 50; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: shakeTranslate; property: "x"; from: -10; to: 10; duration: 50; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: shakeTranslate; property: "x"; from: 10; to: -10; duration: 50; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: shakeTranslate; property: "x"; from: -10; to: 10; duration: 50; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { target: shakeTranslate; property: "x"; from: 10; to: 0; duration: 50; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
// Shadow effect (behind background)
|
||||
NDropShadow {
|
||||
anchors.fill: customBackground
|
||||
source: customBackground
|
||||
autoPaddingEnabled: true
|
||||
z: -1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: customBackground
|
||||
anchors.fill: parent
|
||||
radius: Style.radiusL
|
||||
color: Qt.alpha(Color.mSurface, 0.95)
|
||||
border.color: (flow && (flow.failed || flow.supplementaryIsError)) ? Color.mError : Color.mOutline
|
||||
border.width: Style.borderS
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: Style.animationFast }
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - (Style.marginL * 2)
|
||||
spacing: Style.marginM
|
||||
|
||||
// Header with Icon
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NImageRounded {
|
||||
Layout.preferredWidth: Style.fontSizeXXL * 2
|
||||
Layout.preferredHeight: Style.fontSizeXXL * 2
|
||||
imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) || ((flow && flow.iconName) ? Quickshell.iconPath(flow.iconName) : "")
|
||||
fallbackIcon: "lock"
|
||||
borderWidth: 0
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginXS
|
||||
|
||||
NText {
|
||||
text: polkitWindow.resolvedMessage || (flow ? flow.message : (pluginApi ? pluginApi.tr("window.title") : "Authentication Required"))
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NText {
|
||||
text: flow ? flow.actionId : ""
|
||||
pointSize: Style.fontSizeXS
|
||||
color: Color.mOnSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supplementary Message (Error or prompt)
|
||||
NText {
|
||||
visible: flow && flow.supplementaryMessage !== ""
|
||||
text: flow ? flow.supplementaryMessage : ""
|
||||
pointSize: Style.fontSizeS
|
||||
color: (flow && flow.supplementaryIsError) ? Color.mError : Color.mOnSurfaceVariant
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Input Field
|
||||
NTextInput {
|
||||
id: passwordInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: flow ? flow.inputPrompt : (pluginApi ? pluginApi.tr("prompt.password") : "Password")
|
||||
label: (flow && flow.isResponseRequired) ? (pluginApi ? pluginApi.tr("prompt.password") : "Password") : ""
|
||||
inputItem.echoMode: (flow && !flow.responseVisible) ? TextInput.Password : TextInput.Normal
|
||||
visible: flow && flow.isResponseRequired
|
||||
|
||||
onAccepted: {
|
||||
if (flow) {
|
||||
flow.submit(passwordInput.text)
|
||||
passwordInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Style.marginS
|
||||
spacing: Style.marginM
|
||||
|
||||
Item { Layout.fillWidth: true } // Spacer
|
||||
|
||||
NButton {
|
||||
text: pluginApi ? pluginApi.tr("action.cancel") : "Cancel"
|
||||
backgroundColor: Color.mSurfaceVariant
|
||||
textColor: Color.mOnSurfaceVariant
|
||||
outlined: false
|
||||
onClicked: {
|
||||
if (flow) flow.cancelAuthenticationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: pluginApi ? pluginApi.tr("action.authenticate") : "Authenticate"
|
||||
backgroundColor: Color.mPrimary
|
||||
textColor: Color.mOnPrimary
|
||||
enabled: flow && flow.isResponseRequired
|
||||
onClicked: {
|
||||
if (flow) {
|
||||
flow.submit(passwordInput.text)
|
||||
passwordInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Focus handling
|
||||
Component.onCompleted: {
|
||||
passwordInput.inputItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Polkit Agent
|
||||
|
||||
This plugin provides a Polkit authentication agent for Noctalia. It allows you to authenticate actions that require elevated privileges directly within the shell.
|
||||
|
||||

|
||||
|
||||
## Important
|
||||
|
||||
To use this plugin, you **must disable or uninstall your existing polkit authentication agent** (e.g., `polkit-gnome`, `polkit-kde-agent`, `lxpolkit`, etc).
|
||||
|
||||
Having multiple polkit agents running simultaneously will cause conflicts and prevent this plugin from working correctly.
|
||||
|
||||
> You may need to **restart your session or computer** after enabling this plugin for the changes to take effect and for the new agent to be registered properly.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Authentifizierung erforderlich"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Passwort"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Abbrechen",
|
||||
"authenticate": "Authentifizieren"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Authentifizierung fehlgeschlagen",
|
||||
"message": "Das eingegebene Passwort war falsch."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Authentication Required"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Password"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Cancel",
|
||||
"authenticate": "Authenticate"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Authentication Failed",
|
||||
"message": "The password you entered was incorrect."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Autenticación requerida"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Contraseña"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Cancelar",
|
||||
"authenticate": "Autenticar"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Autenticación fallida",
|
||||
"message": "La contraseña introducida es incorrecta."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Authentification requise"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Mot de passe"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Annuler",
|
||||
"authenticate": "S'authentifier"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Authentification échouée",
|
||||
"message": "Le mot de passe saisi est incorrect."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Hitelesítés szükséges"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Jelszó"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Mégse",
|
||||
"authenticate": "Hitelesítés"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Hitelesítés sikertelen",
|
||||
"message": "A megadott jelszó helytelen volt."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "認証が必要です"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "パスワード"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "キャンセル",
|
||||
"authenticate": "認証"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "認証に失敗しました",
|
||||
"message": "入力されたパスワードが正しくありません。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "인증 필요"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "비밀번호"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "취소",
|
||||
"authenticate": "인증"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "인증 실패",
|
||||
"message": "입력한 비밀번호가 올바르지 않습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Nasname pêwîst e"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Şîfre"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Betal bike",
|
||||
"authenticate": "Nasnameyê bipesend bike"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Nasname têk çû",
|
||||
"message": "Şîfreya ku hûn nivîsandin şaş e."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Authenticatie vereist"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Wachtwoord"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Annuleren",
|
||||
"authenticate": "Authenticeren"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Authenticatie mislukt",
|
||||
"message": "Het ingevoerde wachtwoord was onjuist."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Autentisering kreves"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Passord"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Avbryt",
|
||||
"authenticate": "Autentiser"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Autentisering mislyktes",
|
||||
"message": "Passordet du skrev inn var feil."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Autentisering krevst"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Passord"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Avbryt",
|
||||
"authenticate": "Autentiser"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Autentisering mislukkast",
|
||||
"message": "Passordet du skreiv inn var feil."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Wymagane uwierzytelnienie"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Hasło"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Anuluj",
|
||||
"authenticate": "Uwierzytelnij"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Uwierzytelnienie nie powiodło się",
|
||||
"message": "Wprowadzone hasło jest nieprawidłowe."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Autenticação Necessária"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Senha"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Cancelar",
|
||||
"authenticate": "Autenticar"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Autenticação Falhou",
|
||||
"message": "A senha introduzida está incorreta."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Требуется аутентификация"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Пароль"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Отмена",
|
||||
"authenticate": "Аутентифицировать"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Ошибка аутентификации",
|
||||
"message": "Введен неверный пароль."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Autentisering krävs"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Lösenord"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "Avbryt",
|
||||
"authenticate": "Autentisera"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Autentisering misslyckades",
|
||||
"message": "Det angivna lösenordet var felaktigt."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"window": {
|
||||
"title": "Kimlik Doğrulaması Gerekli"
|
||||
},
|
||||
"prompt": {
|
||||
"password": "Parola"
|
||||
},
|
||||
"action": {
|
||||
"cancel": "İptal",
|
||||
"authenticate": "Doğrula"
|
||||
},
|
||||
"error": {
|
||||
"failed": {
|
||||
"title": "Kimlik Doğrulama Başarısız",
|
||||
"message": "Girdiğiniz parola yanlış."
|
||||
}
|
||||
}
|
||||
}
|
||||