27 Commits

Author SHA1 Message Date
d3d5270889 closes #6 2026-03-03 17:47:45 -05:00
017a2ae5a4 multiline clip name 2026-03-01 17:47:59 -05:00
791abef1ef better auto focus for delete 2026-03-01 17:35:24 -05:00
31cc3079a8 kinda bad, but functional delete on enter 2026-03-01 17:31:14 -05:00
5e50b29625 trimmer jsx cleanup 2026-03-01 17:27:12 -05:00
d37cd773f8 proper closing, metadata file locations moved 2026-03-01 13:48:31 -05:00
801966e8d8 fix duplicate sockets, fix dll build error 2026-03-01 10:23:54 -05:00
39395fd846 system tray and package config 2026-02-28 19:09:45 -05:00
510b92f669 wavesurfer lazy loading 2026-02-28 17:28:49 -05:00
aefb3f2648 Merge branch 'socket' into react_migration 2026-02-28 17:04:44 -05:00
a613b26ba8 remove pycache 2026-02-28 17:04:11 -05:00
7a471041e7 port selection on plugin 2026-02-28 17:01:59 -05:00
ab57d8ef22 socket in client, new set of icons 2026-02-28 11:17:37 -05:00
69c9d80a82 plugin work, page navigation, reticks 2026-02-26 20:02:22 -05:00
8c83819a17 faster socket, clean up on plugin 2026-02-26 17:38:50 -05:00
ad07bf7fe6 remove client test app 2026-02-26 15:52:04 -05:00
bc40f9abe3 socket set up properly 2026-02-26 15:48:41 -05:00
e7f649ae0b * 'Line 1: Added 'using Newtonsoft.Json.Serialization;' to ensure that JsonPropertyAttribute is available, as it is defined in this namespace in Newtonsoft.Json.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:25 -05:00
e34903b20f * 'Line 3: Removed the duplicate using directive for Newtonsoft.Json.Converters.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:18 -05:00
192c959d39 * 'Line 1: Ensure both 'Newtonsoft.Json' and 'Newtonsoft.Json.Converters' namespaces are included, as JsonPropertyAttribute and StringEnumConverter are defined in these namespaces. This resolves the CS0246 error for missing JsonPropertyAttribute.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:12 -05:00
60123d7450 * 'Line 37: Replaced [JsonProperty(PropertyName = path)] with [JsonPropertyName(path)] to use the System.Text.Json.Serialization.JsonPropertyNameAttribute, which is available in .NET 8 and the current project.
Line 40: Replaced [JsonProperty(PropertyName = index)] with [JsonPropertyName(index)] to use the correct attribute from System.Text.Json.Serialization.
Line 42: Replaced [JsonProperty(PropertyName = playtype)] with [JsonPropertyName(playtype)] to use the correct attribute from System.Text.Json.Serialization.
Line 45: Replaced [JsonProperty(PropertyName = volume)] with [JsonPropertyName(volume)] to use the correct attribute from System.Text.Json.Serialization.
Line 5: Replaced the using directive for Newtonsoft.Json with System.Text.Json.Serialization, as the project does not reference Newtonsoft.Json and should use System.Text.Json.Serialization.JsonPropertyNameAttribute instead.' in file 'stream_deck_plugin\ClipTrimDotNet\Player.cs'
2026-02-24 21:16:05 -05:00
8265951bd4 Move assembly metadata to .csproj and clean AssemblyInfo.cs
Assembly metadata such as title, product, copyright, and version information was moved from Properties/AssemblyInfo.cs to the ClipTrimDotNet.csproj file. The corresponding attributes and comments were removed from AssemblyInfo.cs, leaving only the trademark, culture, and GUID attributes. This streamlines project configuration and centralizes assembly information in the project file.
2026-02-24 21:15:31 -05:00
757d5ef1a7 Update package versions in ClipTrimDotNet.csproj
Upgraded several NuGet package dependencies in ClipTrimDotNet.csproj, including Microsoft.Extensions.* packages, System.* packages, and System.Drawing.Common, from version 8.x to 10.x or 9.x where applicable. This ensures compatibility with newer frameworks and libraries, and may provide bug fixes and performance improvements. No other files were modified.
2026-02-24 21:15:29 -05:00
d78c49d0ad Update ClipTrimDotNet.csproj dependencies to latest versions
Modernized the ClipTrimDotNet.csproj by upgrading several Microsoft.Extensions and System.* package references to their latest 8.x versions, replacing or removing older references. Added new CoreWCF packages and System.Configuration.ConfigurationManager for enhanced WCF and configuration support. Cleaned up unused or redundant references to streamline the project dependencies.
2026-02-24 21:14:40 -05:00
089023e7cf Refactor ClipTrimDotNet.csproj to SDK style and .NET 8
Converted the project file to the modern SDK-style format, targeting .NET 8.0 instead of .NET Framework 4.8. Replaced explicit assembly references and manual NuGet package management with <PackageReference> elements for all dependencies. Removed legacy MSBuild properties, imports, and targets, simplifying the project structure. Updated build events and ensured content files are copied as needed. This modernization improves maintainability and compatibility with current .NET tooling.
2026-02-24 21:14:28 -05:00
db97747f2e Commit upgrade plan 2026-02-24 21:13:55 -05:00
1e7141c43f some tests 2026-02-24 21:11:26 -05:00
123 changed files with 3792 additions and 2611 deletions

70
.github/upgrades/dotnet-upgrade-plan.md vendored Normal file
View File

@ -0,0 +1,70 @@
# .NET 8.0 Upgrade Plan
## Execution Steps
Execute steps below sequentially one by one in the order they are listed.
1. Validate that an .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed.
2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade.
3. Upgrade ClipTrimDotNet\ClipTrimDotNet.csproj
4. Run unit tests to validate upgrade in the projects listed below:
- ClientTest\ClientTest.csproj
## Settings
This section contains settings and data used by execution steps.
### Excluded projects
| Project name | Description |
|:-----------------------------------------------|:---------------------------:|
### Aggregate NuGet packages modifications across all projects
NuGet packages used across all selected projects or their dependencies that need version update in projects that reference them.
| Package Name | Current Version | New Version | Description |
|:------------------------------------|:---------------:|:-----------:|:----------------------------------------------|
| Microsoft.Bcl.AsyncInterfaces | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| Microsoft.Extensions.DependencyInjection | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.2 | 8.0.2 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Logging | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Logging.Abstractions | 10.0.2 | 8.0.3 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Options | 10.0.2 | 8.0.2 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Primitives | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Diagnostics.DiagnosticSource | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| System.Drawing.Common | 9.0.10 | 8.0.24 | Recommended for .NET 8.0 |
| System.IO.Pipelines | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Security.AccessControl | 4.7.0 | 6.0.1 | Recommended for .NET 8.0 |
| System.Text.Encodings.Web | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Text.Json | 10.0.2 | 8.0.6 | Recommended for .NET 8.0 |
| Microsoft.Win32.Registry | 4.7.0 | | Functionality included with framework |
| System.Buffers | 4.6.1 | | Functionality included with framework |
| System.IO | 4.3.0 | | Functionality included with framework |
| System.Memory | 4.6.3 | | Functionality included with framework |
| System.Net.Http | 4.3.4 | | Functionality included with framework |
| System.Numerics.Vectors | 4.6.1 | | Functionality included with framework |
| System.Runtime | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Algorithms | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Encoding | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Primitives | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.X509Certificates | 4.3.0 | | Functionality included with framework |
| System.Security.Principal.Windows | 4.7.0 | | Functionality included with framework |
| System.Threading.Tasks.Extensions | 4.6.3 | | Functionality included with framework |
| System.ValueTuple | 4.6.1 | | Functionality included with framework |
### Project upgrade details
#### ClipTrimDotNet\ClipTrimDotNet.csproj modifications
Project properties changes:
- Target framework should be changed from `.NETFramework,Version=v4.8` to `net8.0`
- Project file should be converted to SDK-style
NuGet packages changes:
- Update all packages listed in the NuGet packages table above as recommended
- Remove packages whose functionality is now included with the framework
Other changes:
- Ensure compatibility with .NET 8.0 APIs and features
- Update code as needed for breaking changes

View File

@ -1 +1,2 @@
recordings/ recordings/
__pycache__/

View File

@ -2,7 +2,14 @@
{ {
"name": "Uncategorized", "name": "Uncategorized",
"id": 0, "id": 0,
"clips": [] "clips": [
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_210924.wav",
"name": "Clip 20260228_210924",
"playbackType": "playStop",
"volume": 1.0
}
]
}, },
{ {
"name": "Test", "name": "Test",
@ -12,23 +19,6 @@
{ {
"name": "New", "name": "New",
"id": 2, "id": 2,
"clips": [ "clips": []
{
"endTime": 30,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav",
"name": "Pee pee\npoo poo",
"playbackType": "playOverlap",
"startTime": 27.76674010920584,
"volume": 0.25
},
{
"endTime": 27.516843118383072,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
"name": "Clip 20260220_200442",
"playbackType": "playOverlap",
"startTime": 25.120307988450435,
"volume": 0.64
}
]
} }
] ]

View File

@ -1,7 +1,6 @@
sounddevice==0.5.1 Flask==3.1.3
numpy==1.22.3 flask_cors==6.0.2
python-osc==1.9.3 flask_socketio==5.6.1
scipy==1.10.1 numpy==2.4.2
comtypes==1.4.8 scipy==1.17.1
pycaw==20240210 sounddevice==0.5.5
Flask==3.1.2

View File

@ -29,6 +29,8 @@ class AudioIO:
sd.default.latency = 'low' sd.default.latency = 'low'
self.socket = None
self.in_stream = sd.InputStream( self.in_stream = sd.InputStream(
callback=self.record_callback callback=self.record_callback
) )
@ -119,7 +121,7 @@ class AudioIO:
} }
meta.add_clip_to_collection("Uncategorized", clip_metadata ) meta.add_clip_to_collection("Uncategorized", clip_metadata )
self.socket.emit('new_clip', clip_metadata)
return clip_metadata return clip_metadata

View File

@ -14,13 +14,30 @@ from routes.recording import recording_bp
from routes.device import device_bp from routes.device import device_bp
from routes.metadata import metadata_bp from routes.metadata import metadata_bp
from routes.settings import settings_bp from routes.settings import settings_bp
from flask_socketio import SocketIO from flask_socketio import SocketIO, emit
import threading import threading
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# socketio = SocketIO(app, cors_allowed_origins="*") socketio = SocketIO(app, cors_allowed_origins="*", logger=True, engineio_logger=True, async_mode='eventlet')
# CORS(socketio)
@socketio.on('connect')
def handle_connect():
print("Client connected")
emit('full_data', MetaDataManager().collections)
@socketio.on('record_clip')
def record_clip():
io = AudioIO()
io.save_last_n_seconds();
@socketio.on('play_clip')
def play_clip(data):
io = AudioIO()
print(f"Received play_clip event with data: {data}")
if data:
io.play_clip(data)
def main(): def main():
# Create argument parser # Create argument parser
@ -36,6 +53,9 @@ def main():
args = parser.parse_args() args = parser.parse_args()
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()
settings = SettingsManager() settings = SettingsManager()
meta = MetaDataManager()
# Ensure save path exists # Ensure save path exists
os.makedirs(settings.get_settings('save_path'), exist_ok=True) os.makedirs(settings.get_settings('save_path'), exist_ok=True)
@ -43,13 +63,19 @@ def main():
io = AudioIO() io = AudioIO()
io.start_recording() io.start_recording()
# settings.socket = socketio
io.socket = socketio
meta.socket = socketio
# Register blueprints # Register blueprints
app.register_blueprint(recording_bp) app.register_blueprint(recording_bp)
app.register_blueprint(device_bp) app.register_blueprint(device_bp)
app.register_blueprint(metadata_bp) app.register_blueprint(metadata_bp)
app.register_blueprint(settings_bp) app.register_blueprint(settings_bp)
app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True) print(f"Starting Flask server on port {settings.get_settings('http_port')}")
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) # app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True)
socketio.run(app, host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=False, allow_unsafe_werkzeug=True)

View File

@ -1,5 +1,6 @@
import os import os
import json import json
from platformdirs import user_data_dir
class MetaDataManager: class MetaDataManager:
_instance = None _instance = None
@ -10,13 +11,17 @@ class MetaDataManager:
cls._instance.init() cls._instance.init()
return cls._instance return cls._instance
def init(self): def init(self):
self.socket = None
# read metadata file from executing directory # read metadata file from executing directory
self.metadata_file = os.path.join(os.getcwd(), "metadata.json") file_path = user_data_dir("ClipTrim")
os.makedirs(file_path, exist_ok=True)
print(file_path)
self.metadata_file = os.path.join(file_path, "metadata.json")
if os.path.exists(self.metadata_file): if os.path.exists(self.metadata_file):
with open(self.metadata_file, "r") as f: with open(self.metadata_file, "r") as f:
self.collections = json.load(f) self.collections = json.load(f)
else: else:
self.collections = {} self.collections = []
if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None: if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
self.collections.append({"name": "Uncategorized", "id": 0, "clips": []}) self.collections.append({"name": "Uncategorized", "id": 0, "clips": []})
self.save_metadata() self.save_metadata()
@ -40,6 +45,8 @@ class MetaDataManager:
if collection is None: if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.") raise ValueError(f"Collection '{collection_name}' does not exist.")
collection["clips"].append(clip_metadata) collection["clips"].append(clip_metadata)
if not self.socket is None:
self.socket.emit('collection_updated', collection)
self.save_metadata() self.save_metadata()
def remove_clip_from_collection(self, collection_name, clip_metadata): def remove_clip_from_collection(self, collection_name, clip_metadata):
@ -55,11 +62,18 @@ class MetaDataManager:
clip for clip in collection["clips"] clip for clip in collection["clips"]
if clip.get("filename") != clip_metadata.get("filename") if clip.get("filename") != clip_metadata.get("filename")
] ]
if not self.socket is None:
self.socket.emit('collection_updated', collection)
self.save_metadata() self.save_metadata()
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata): def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
self.remove_clip_from_collection(source_collection, clip_metadata) self.remove_clip_from_collection(source_collection, clip_metadata)
self.add_clip_to_collection(target_collection, clip_metadata) self.add_clip_to_collection(target_collection, clip_metadata)
if not self.socket is None:
self.socket.emit('collection_updated', source_collection)
self.socket.emit('collection_updated', target_collection)
def edit_clip_in_collection(self, collection_name, new_clip_metadata): def edit_clip_in_collection(self, collection_name, new_clip_metadata):
collection = next((c for c in self.collections if c.get("name") == collection_name), None) collection = next((c for c in self.collections if c.get("name") == collection_name), None)
@ -71,6 +85,8 @@ class MetaDataManager:
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.") raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
collection["clips"][index] = new_clip_metadata collection["clips"][index] = new_clip_metadata
if not self.socket is None:
self.socket.emit('collection_updated', collection)
self.save_metadata() self.save_metadata()
def get_collections(self): def get_collections(self):
@ -93,6 +109,8 @@ class MetaDataManager:
raise ValueError("New order contains clips that do not exist in the collection.") raise ValueError("New order contains clips that do not exist in the collection.")
collection["clips"] = new_order collection["clips"] = new_order
if not self.socket is None:
self.socket.emit('collection_updated', collection)
self.save_metadata() self.save_metadata()
def save_metadata(self): def save_metadata(self):

View File

@ -98,3 +98,9 @@ def edit_clip_in_collection():
return jsonify({'status': 'success', 'collections': collections}) return jsonify({'status': 'success', 'collections': collections})
except ValueError as e: except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400
@metadata_bp.route('/ws/test', methods=['POST'])
def test_websocket():
MetaDataManager().socket.emit('test_event', {'data': 'Test message from metadata route'})
return jsonify({'status': 'success'})

View File

@ -29,3 +29,4 @@ def set_all_settings():
return jsonify({'status': 'success', 'settings': settings}) return jsonify({'status': 'success', 'settings': settings})
except ValueError as e: except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400

View File

@ -1,5 +1,7 @@
import os import os
import json import json
from platformdirs import user_data_dir
from audio_io import AudioIO from audio_io import AudioIO
from windows_audio import WindowsAudioManager from windows_audio import WindowsAudioManager
@ -14,7 +16,9 @@ class SettingsManager:
def init(self): def init(self):
# read settings file from executing directory # read settings file from executing directory
print("Initializing SettingsManager", os.getcwd()) print("Initializing SettingsManager", os.getcwd())
self.settings_file = os.path.join(os.getcwd(), "settings.json") file_path = user_data_dir("ClipTrim")
os.makedirs(file_path, exist_ok=True)
self.settings_file = os.path.join(file_path, "settings.json")
if os.path.exists(self.settings_file): if os.path.exists(self.settings_file):
with open(self.settings_file, "r") as f: with open(self.settings_file, "r") as f:
self.settings = json.load(f) self.settings = json.load(f)
@ -22,7 +26,7 @@ class SettingsManager:
self.settings = { self.settings = {
"input_device": None, "input_device": None,
"output_device": None, "output_device": None,
"save_path": os.path.join(os.getcwd(), "recordings"), "save_path": os.path.join(file_path, "recordings"),
"recording_length": 15 "recording_length": 15
} }
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()

View File

@ -1,9 +1,5 @@
import sounddevice as sd import sounddevice as sd
import numpy as np import numpy as np
import comtypes
import comtypes.client
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
import json import json
class WindowsAudioManager: class WindowsAudioManager:

View File

@ -1,4 +1,4 @@
const tailwindcss = require('@tailwindcss/postcss'); const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer'); const autoprefixer = require('autoprefixer');
module.exports = { module.exports = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
electron-ui/dll_err.txt Normal file

Binary file not shown.

View File

@ -1,10 +1,12 @@
{ {
"name": "electron-react-boilerplate", "name": "cliptrim-ui",
"version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "electron-react-boilerplate", "name": "cliptrim-ui",
"version": "2.0.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -18,7 +20,6 @@
"@mui/icons-material": "^7.3.7", "@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7", "@mui/material": "^7.3.7",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/cli": "^4.1.18",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@wavesurfer/react": "^1.0.12", "@wavesurfer/react": "^1.0.12",
"electron-debug": "^4.1.0", "electron-debug": "^4.1.0",
@ -31,13 +32,13 @@
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"socketio": "^1.0.0", "socketio": "^1.0.0",
"tailwindcss": "^4.1.18",
"wavesurfer.js": "^7.12.1" "wavesurfer.js": "^7.12.1"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^3.7.1", "@electron/rebuild": "^3.7.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@tailwindcss/cli": "^4.2.1",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2", "@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
@ -49,7 +50,7 @@
"@types/webpack-bundle-analyzer": "^4.7.0", "@types/webpack-bundle-analyzer": "^4.7.0",
"@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1", "@typescript-eslint/parser": "^8.26.1",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.27",
"browserslist-config-erb": "^0.0.3", "browserslist-config-erb": "^0.0.3",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
@ -81,7 +82,7 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-loader": "^8.2.0", "postcss-loader": "^8.2.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-refresh": "^0.16.0", "react-refresh": "^0.16.0",
"react-test-renderer": "^19.0.0", "react-test-renderer": "^19.0.0",
@ -89,6 +90,7 @@
"sass": "^1.86.0", "sass": "^1.86.0",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"tailwindcss": "^4.2.1",
"terser-webpack-plugin": "^5.3.14", "terser-webpack-plugin": "^5.3.14",
"ts-jest": "^29.2.6", "ts-jest": "^29.2.6",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
@ -4627,6 +4629,7 @@
"version": "2.5.6", "version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -4665,6 +4668,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4685,6 +4689,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4705,6 +4710,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4725,6 +4731,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4745,6 +4752,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4765,6 +4773,7 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4785,6 +4794,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4805,6 +4815,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4825,6 +4836,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4845,6 +4857,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4865,6 +4878,7 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4885,6 +4899,7 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4905,6 +4920,7 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@ -4922,12 +4938,14 @@
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@parcel/watcher/node_modules/picomatch": { "node_modules/@parcel/watcher/node_modules/picomatch": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -5561,27 +5579,286 @@
} }
}, },
"node_modules/@tailwindcss/cli": { "node_modules/@tailwindcss/cli": {
"version": "4.1.18", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz",
"integrity": "sha512-sMZ+lZbDyxwjD2E0L7oRUjJ01Ffjtme5OtjvvnC+cV4CEDcbqzbp25TCpxHj6kWLU9+DlqJOiNgSOgctC2aZmg==", "integrity": "sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@parcel/watcher": "^2.5.1", "@parcel/watcher": "^2.5.1",
"@tailwindcss/node": "4.1.18", "@tailwindcss/node": "4.2.1",
"@tailwindcss/oxide": "4.1.18", "@tailwindcss/oxide": "4.2.1",
"enhanced-resolve": "^5.18.3", "enhanced-resolve": "^5.19.0",
"mri": "^1.2.0", "mri": "^1.2.0",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"tailwindcss": "4.1.18" "tailwindcss": "4.2.1"
}, },
"bin": { "bin": {
"tailwindcss": "dist/index.mjs" "tailwindcss": "dist/index.mjs"
} }
}, },
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
"integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.19.0",
"jiti": "^2.6.1",
"lightningcss": "1.31.1",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.2.1"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
"integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.2.1",
"@tailwindcss/oxide-darwin-arm64": "4.2.1",
"@tailwindcss/oxide-darwin-x64": "4.2.1",
"@tailwindcss/oxide-freebsd-x64": "4.2.1",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
"@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
"@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
"@tailwindcss/oxide-linux-x64-musl": "4.2.1",
"@tailwindcss/oxide-wasm32-wasi": "4.2.1",
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
"@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
"integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
"integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
"integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
"integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
"integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
"integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
"integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
"integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
"integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
"integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.8.1",
"@emnapi/runtime": "^1.8.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.1",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
"integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
"integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/cli/node_modules/enhanced-resolve": { "node_modules/@tailwindcss/cli/node_modules/enhanced-resolve": {
"version": "5.19.0", "version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.4", "graceful-fs": "^4.2.4",
@ -5591,6 +5868,267 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/@tailwindcss/cli/node_modules/lightningcss": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
"integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.31.1",
"lightningcss-darwin-arm64": "1.31.1",
"lightningcss-darwin-x64": "1.31.1",
"lightningcss-freebsd-x64": "1.31.1",
"lightningcss-linux-arm-gnueabihf": "1.31.1",
"lightningcss-linux-arm64-gnu": "1.31.1",
"lightningcss-linux-arm64-musl": "1.31.1",
"lightningcss-linux-x64-gnu": "1.31.1",
"lightningcss-linux-x64-musl": "1.31.1",
"lightningcss-win32-arm64-msvc": "1.31.1",
"lightningcss-win32-x64-msvc": "1.31.1"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-android-arm64": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
"integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-darwin-arm64": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
"integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-darwin-x64": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
"integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-freebsd-x64": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
"integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
"integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
"integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-linux-arm64-musl": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
"integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-linux-x64-gnu": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
"integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-linux-x64-musl": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
"integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
"integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/cli/node_modules/lightningcss-win32-x64-msvc": {
"version": "1.31.1",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
"integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
@ -5619,6 +6157,12 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/@tailwindcss/node/node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
},
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
@ -5860,6 +6404,12 @@
"tailwindcss": "4.1.18" "tailwindcss": "4.1.18"
} }
}, },
"node_modules/@tailwindcss/postcss/node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
},
"node_modules/@teamsupercell/typings-for-css-modules-loader": { "node_modules/@teamsupercell/typings-for-css-modules-loader": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/@teamsupercell/typings-for-css-modules-loader/-/typings-for-css-modules-loader-2.5.2.tgz", "resolved": "https://registry.npmjs.org/@teamsupercell/typings-for-css-modules-loader/-/typings-for-css-modules-loader-2.5.2.tgz",
@ -8345,9 +8895,9 @@
} }
}, },
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.24", "version": "10.4.27",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -8366,7 +8916,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"browserslist": "^4.28.1", "browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766", "caniuse-lite": "^1.0.30001774",
"fraction.js": "^5.3.4", "fraction.js": "^5.3.4",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0" "postcss-value-parser": "^4.2.0"
@ -9281,9 +9831,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001767", "version": "1.0.30001775",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
"integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -15402,6 +15952,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@ -15467,6 +16018,7 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
@ -18556,6 +19108,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@ -19792,9 +20345,9 @@
} }
}, },
"node_modules/postcss-loader": { "node_modules/postcss-loader": {
"version": "8.2.0", "version": "8.2.1",
"resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.1.tgz",
"integrity": "sha512-tHX+RkpsXVcc7st4dSdDGliI+r4aAQDuv+v3vFYHixb6YgjreG5AG4SEB0kDK8u2s6htqEEpKlkhSBUTvWKYnA==", "integrity": "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -19810,7 +20363,7 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
}, },
"peerDependencies": { "peerDependencies": {
"@rspack/core": "0.x || 1.x", "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
"postcss": "^7.0.0 || ^8.0.1", "postcss": "^7.0.0 || ^8.0.1",
"webpack": "^5.0.0" "webpack": "^5.0.0"
}, },
@ -23232,9 +23785,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.1.18", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {

View File

@ -1,5 +1,5 @@
{ {
"name": "electron-react-boilerplate", "name": "cliptrim-ui",
"description": "A foundation for scalable desktop apps", "description": "A foundation for scalable desktop apps",
"keywords": [ "keywords": [
"electron", "electron",
@ -12,27 +12,7 @@
"hot", "hot",
"reload" "reload"
], ],
"homepage": "https://github.com/electron-react-boilerplate/electron-react-boilerplate#readme",
"bugs": {
"url": "https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/electron-react-boilerplate/electron-react-boilerplate.git"
},
"license": "MIT", "license": "MIT",
"author": {
"name": "Electron React Boilerplate Maintainers",
"email": "electronreactboilerplate@gmail.com",
"url": "https://electron-react-boilerplate.js.org"
},
"contributors": [
{
"name": "Amila Welihinda",
"email": "amilajack@gmail.com",
"url": "https://github.com/amilajack"
}
],
"main": "./.erb/dll/main.bundle.dev.js", "main": "./.erb/dll/main.bundle.dev.js",
"scripts": { "scripts": {
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@ -42,14 +22,15 @@
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll", "postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx", "lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
"lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:fix": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll", "package": "npm run build && electron-builder build --publish never && npm run build:dll",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.dev.ts", "prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.main.dev.ts",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer", "start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
"start:main": "concurrently -k -P \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon . -- {@}\" --", "start:main": "concurrently -k -P \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon . -- {@}\" --",
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts", "start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", "start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true NODE_OPTIONS=\"-r ts-node/register --no-warnings\" webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest" "test": "jest",
"build:win": "electron-builder --win"
}, },
"browserslist": [ "browserslist": [
"extends browserslist-config-erb" "extends browserslist-config-erb"
@ -111,7 +92,6 @@
"@mui/icons-material": "^7.3.7", "@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7", "@mui/material": "^7.3.7",
"@reduxjs/toolkit": "^2.11.2", "@reduxjs/toolkit": "^2.11.2",
"@tailwindcss/cli": "^4.1.18",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@wavesurfer/react": "^1.0.12", "@wavesurfer/react": "^1.0.12",
"electron-debug": "^4.1.0", "electron-debug": "^4.1.0",
@ -124,13 +104,13 @@
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"socketio": "^1.0.0", "socketio": "^1.0.0",
"tailwindcss": "^4.1.18",
"wavesurfer.js": "^7.12.1" "wavesurfer.js": "^7.12.1"
}, },
"devDependencies": { "devDependencies": {
"@electron/rebuild": "^3.7.1", "@electron/rebuild": "^3.7.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
"@tailwindcss/cli": "^4.2.1",
"@teamsupercell/typings-for-css-modules-loader": "^2.5.2", "@teamsupercell/typings-for-css-modules-loader": "^2.5.2",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
@ -142,7 +122,7 @@
"@types/webpack-bundle-analyzer": "^4.7.0", "@types/webpack-bundle-analyzer": "^4.7.0",
"@typescript-eslint/eslint-plugin": "^8.26.1", "@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1", "@typescript-eslint/parser": "^8.26.1",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.27",
"browserslist-config-erb": "^0.0.3", "browserslist-config-erb": "^0.0.3",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
@ -174,7 +154,7 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"mini-css-extract-plugin": "^2.9.2", "mini-css-extract-plugin": "^2.9.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-loader": "^8.2.0", "postcss-loader": "^8.2.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"react-refresh": "^0.16.0", "react-refresh": "^0.16.0",
"react-test-renderer": "^19.0.0", "react-test-renderer": "^19.0.0",
@ -182,6 +162,7 @@
"sass": "^1.86.0", "sass": "^1.86.0",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"tailwindcss": "^4.2.1",
"terser-webpack-plugin": "^5.3.14", "terser-webpack-plugin": "^5.3.14",
"ts-jest": "^29.2.6", "ts-jest": "^29.2.6",
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
@ -195,9 +176,10 @@
"webpack-dev-server": "^5.2.0", "webpack-dev-server": "^5.2.0",
"webpack-merge": "^6.0.1" "webpack-merge": "^6.0.1"
}, },
"version": "2.0.0",
"build": { "build": {
"productName": "ElectronReact", "productName": "ClipTrim",
"appId": "org.erb.ElectronReact", "appId": "com.michalcourson.cliptrimserivce",
"asar": true, "asar": true,
"afterSign": ".erb/scripts/notarize.js", "afterSign": ".erb/scripts/notarize.js",
"asarUnpack": "**\\*.{node,dll}", "asarUnpack": "**\\*.{node,dll}",
@ -238,7 +220,8 @@
"win": { "win": {
"target": [ "target": [
"nsis" "nsis"
] ],
"icon": "build/icon.ico"
}, },
"linux": { "linux": {
"target": [ "target": [
@ -252,13 +235,18 @@
"output": "release/build" "output": "release/build"
}, },
"extraResources": [ "extraResources": [
{
"from": "../audio-service",
"to": "audio-service",
"filter": [
"**/*",
"!**/*.json",
"!**/recordings/*",
"!**/src/__pycache__/*"
]
},
"./assets/**" "./assets/**"
], ]
"publish": {
"provider": "github",
"owner": "electron-react-boilerplate",
"repo": "electron-react-boilerplate"
}
}, },
"collective": { "collective": {
"url": "https://opencollective.com/electron-react-boilerplate-594" "url": "https://opencollective.com/electron-react-boilerplate-594"

View File

@ -1,12 +1,12 @@
{ {
"name": "electron-react-boilerplate", "name": "cliptrim",
"version": "4.6.0", "version": "2.0.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "electron-react-boilerplate", "name": "cliptrim",
"version": "4.6.0", "version": "2.0.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT" "license": "MIT"
} }

View File

@ -1,13 +1,8 @@
{ {
"name": "electron-react-boilerplate", "name": "cliptrim",
"version": "4.6.0", "version": "2.0.2",
"description": "A foundation for scalable desktop apps", "description": "Clip and trim",
"license": "MIT", "license": "MIT",
"author": {
"name": "Electron React Boilerplate Maintainers",
"email": "electronreactboilerplate@gmail.com",
"url": "https://github.com/electron-react-boilerplate"
},
"main": "./dist/main/main.js", "main": "./dist/main/main.js",
"scripts": { "scripts": {
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js", "rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

View File

@ -1,8 +1,8 @@
import { ipcMain } from 'electron'; import { dialog, ipcMain } from 'electron';
import fs from 'fs'; import fs from 'fs';
import AudioChannels from './channels'; import AudioChannels from './channels';
import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types'; import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types';
import PythonSubprocessManager from '../../main/service'; import PythonSubprocessManager from '../main/service';
export default function registerAudioIpcHandlers() { export default function registerAudioIpcHandlers() {
ipcMain.handle( ipcMain.handle(

View File

@ -1,8 +0,0 @@
const SettingsChannels = {
GET_DEFAULTS: 'settings:get-defaults',
GET_SETTINGS: 'settings:get-settings',
SET_SETTINGS: 'settings:set-settings',
GET_INPUT_DEVICES: 'settings:get-input-devices',
} as const;
export default SettingsChannels;

View File

@ -10,14 +10,24 @@
*/ */
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { app, BrowserWindow, shell, ipcMain } from 'electron'; import {
app,
BrowserWindow,
shell,
ipcMain,
Tray,
Menu,
dialog,
} from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import log from 'electron-log'; import log from 'electron-log';
import MenuBuilder from './menu'; import MenuBuilder from './menu';
import { resolveHtmlPath } from './util'; import { resolveHtmlPath } from './util';
import registerFileIpcHandlers from '../ipc/audio/main'; import registerFileIpcHandlers from '../ipc/main';
import PythonSubprocessManager from './service'; import PythonSubprocessManager from './service';
const pythonManager = new PythonSubprocessManager('src/main.py');
class AppUpdater { class AppUpdater {
constructor() { constructor() {
log.transports.file.level = 'info'; log.transports.file.level = 'info';
@ -26,7 +36,8 @@ class AppUpdater {
} }
} }
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow;
let tray: Tray | null = null;
ipcMain.on('ipc-example', async (event, arg) => { ipcMain.on('ipc-example', async (event, arg) => {
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`; const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
@ -76,7 +87,7 @@ const createWindow = async () => {
show: false, show: false,
width: 1024, width: 1024,
height: 728, height: 728,
icon: getAssetPath('icon.png'), icon: getAssetPath('icon.png'), // Set app icon
webPreferences: { webPreferences: {
preload: app.isPackaged preload: app.isPackaged
? path.join(__dirname, 'preload.js') ? path.join(__dirname, 'preload.js')
@ -97,12 +108,30 @@ const createWindow = async () => {
} }
}); });
mainWindow.on('closed', () => { mainWindow.on('close', (event) => {
mainWindow = null; console.log('close event triggered');
event.preventDefault();
mainWindow.hide();
}); });
const menuBuilder = new MenuBuilder(mainWindow); const menuBuilder = new MenuBuilder(mainWindow);
menuBuilder.buildMenu(); menuBuilder.buildMenu();
registerFileIpcHandlers();
ipcMain.handle('select-directory', async () => {
try {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], // Key property to select a folder
});
if (!result.canceled && result.filePaths.length > 0) {
// Send the selected directory path back to the renderer process
return result.filePaths[0];
}
return null;
} catch (err: any) {
return { error: err.message };
}
});
// Open urls in the user's browser // Open urls in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => { mainWindow.webContents.setWindowOpenHandler((edata) => {
@ -110,14 +139,34 @@ const createWindow = async () => {
return { action: 'deny' }; return { action: 'deny' };
}); });
registerFileIpcHandlers();
const pythonManager = new PythonSubprocessManager('src/main.py');
pythonManager.start();
// Remove this if your app does not use auto updates // Remove this if your app does not use auto updates
// eslint-disable-next-line // eslint-disable-next-line
new AppUpdater(); new AppUpdater();
console.log('asset path: ', getAssetPath('tray_icon.png'));
tray = new Tray(getAssetPath('tray_icon.png'));
const contextMenu = Menu.buildFromTemplate([
{
label: 'Show',
click: () => {
mainWindow?.show();
},
},
{
label: 'Quit',
click: () => {
pythonManager.stop();
tray?.destroy();
mainWindow.close();
mainWindow.destroy();
app.quit();
},
},
]);
tray.setToolTip('ClipTrim');
tray.setContextMenu(contextMenu);
tray.on('double-click', () => {
mainWindow?.show();
});
}; };
/** /**
@ -127,14 +176,19 @@ const createWindow = async () => {
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even // Respect the OSX convention of having the application in memory even
// after all windows have been closed // after all windows have been closed
if (process.platform !== 'darwin') { // pythonManager.stop();
app.quit(); // Do not quit app, keep tray active
} // if (process.platform !== 'darwin') {
// app.quit();
// }
}); });
app app
.whenReady() .whenReady()
.then(() => { .then(() => {
// if (app.isPackaged) {
pythonManager.start();
// }
createWindow(); createWindow();
app.on('activate', () => { app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the

View File

@ -1,8 +1,8 @@
// Disable no-unused-vars, broken for spread args // Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */ /* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import { LoadAudioBufferArgs } from '../ipc/audio/types'; import { LoadAudioBufferArgs } from '../ipc/types';
import AudioChannels from '../ipc/audio/channels'; import AudioChannels from '../ipc/channels';
// import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API // import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API
export type Channels = 'ipc-example'; export type Channels = 'ipc-example';

View File

@ -40,10 +40,10 @@ export default class PythonSubprocessManager {
}, },
); );
this.process.stdout.on('data', (data: Buffer) => { this.process.stdout.on('data', (data: Buffer) => {
console.log(`Python stdout: ${data.toString()}`); // console.log(`Python stdout: ${data.toString()}`);
}); });
this.process.stderr.on('data', (data: Buffer) => { this.process.stderr.on('data', (data: Buffer) => {
// console.error(`Python stderr: ${data.toString()}`); // console.error(`Python stderr: ${data.toString()}`);
const lines = data.toString().split('\n'); const lines = data.toString().split('\n');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const line of lines) { for (const line of lines) {
@ -63,6 +63,8 @@ export default class PythonSubprocessManager {
stop(): void { stop(): void {
if (this.process) { if (this.process) {
// for some reason, process.kill() doens't work well with flask. todo: investigate further
// spawn('taskkill', ['/pid', `${this.process.pid}`, '/f', '/t']);
this.process.kill(); this.process.kill();
this.process = null; this.process = null;
} }

View File

@ -67,32 +67,11 @@ const metadataSlice = createSlice({
targetState.clips.push(clip); targetState.clips.push(clip);
} }
}, },
addNewClips(state, action) { addNewClip(state, action) {
const { collections } = action.payload; const { clip } = action.payload;
Object.keys(collections).forEach((collection) => { state.collections.forEach((collection) => {
const collectionState = state.collections.find( if (collection.name === 'Uncategorized') {
(col) => col.name === collection, collection.clips.push(clip);
);
if (!collectionState) {
state.collections.push({
name: collection,
id: Date.now(),
clips: [],
});
}
const existingFilenames = new Set(
state.collections
.find((col) => col.name === collection)
?.clips.map((clip) => clip.filename) || [],
);
const newClips = collections[collection].filter(
(clip: ClipMetadata) => !existingFilenames.has(clip.filename),
);
// const collectionState = state.collections.find(
// (col) => col.name === collection,
// );
if (collectionState) {
collectionState.clips.push(...newClips);
} }
}); });
}, },
@ -113,6 +92,6 @@ export type RootState = ReturnType<AppStore['getState']>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']; export type AppDispatch = AppStore['dispatch'];
export const { setCollections, addNewClips, addCollection } = export const { setCollections, addNewClip, addCollection } =
metadataSlice.actions; metadataSlice.actions;
export default metadataSlice.reducer; export default metadataSlice.reducer;

View File

@ -2,6 +2,9 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* @import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities'; */
/* /*
* @NOTE: Prepend a `~` to css file paths that are in your node_modules * @NOTE: Prepend a `~` to css file paths that are in your node_modules
* See https://github.com/webpack-contrib/sass-loader#imports * See https://github.com/webpack-contrib/sass-loader#imports

View File

@ -7,6 +7,7 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import io from 'socket.io-client';
// import 'tailwindcss/tailwind.css'; // import 'tailwindcss/tailwind.css';
import './App.css'; import './App.css';
import ClipList from './components/ClipList'; import ClipList from './components/ClipList';
@ -14,7 +15,7 @@ import { useAppDispatch, useAppSelector } from './hooks';
import { store } from '../redux/main'; import { store } from '../redux/main';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import SettingsPage from './Settings'; import SettingsPage from './Settings';
import apiFetch from './api'; import { apiFetch, getBaseUrl } from './api';
function MainPage() { function MainPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -28,19 +29,40 @@ function MainPage() {
const [newCollectionName, setNewCollectionName] = useState<string>(''); const [newCollectionName, setNewCollectionName] = useState<string>('');
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => {}, []);
useEffect(() => { useEffect(() => {
const fetchMetadata = async () => { let newSocket: any = null;
try { const initializeSocket = async () => {
const response = await apiFetch('meta'); const baseUrl = await getBaseUrl();
const data = await response.json(); newSocket = io(baseUrl);
dispatch({ type: 'metadata/setAllData', payload: data }); newSocket.on('connect', () => {
} catch (error) { console.log('Connected to WebSocket server');
console.error('Error fetching metadata:', error); });
newSocket.on('full_data', (data: any) => {
console.log('Received full_data from server:', data);
dispatch({
type: 'metadata/setAllData',
payload: { collections: data },
});
});
newSocket.on('new_clip', (data: any) => {
console.log('Received new_clips from server:', data);
dispatch({
type: 'metadata/addNewClip',
payload: { clip: data },
});
});
};
initializeSocket();
return () => {
if (newSocket) {
newSocket.off('connect');
newSocket.off('full_data');
newSocket.off('new_clip');
newSocket.disconnect();
} }
}; };
fetchMetadata();
const intervalId = setInterval(fetchMetadata, 5000);
return () => clearInterval(intervalId);
}, [dispatch]); }, [dispatch]);
useEffect(() => { useEffect(() => {

View File

@ -1,11 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
// import { ipcRenderer } from 'electron';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import './App.css'; import './App.css';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import apiFetch from './api'; import { IconButton } from '@mui/material';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { apiFetch } from './api';
type AudioDevice = { type AudioDevice = {
index: number; index: number;
@ -123,18 +126,22 @@ export default function SettingsPage() {
}; };
const handleFolderChange = async () => { const handleFolderChange = async () => {
// Replace with actual folder picker await window.electron.ipcRenderer
// Example: const folder = await window.api.selectFolder(); .invoke('select-directory')
// const folder = window.prompt( .then((result) => {
// 'Enter output folder path:', if (result) {
// settings.outputFolder, setSettings((prev) => ({
// ); ...prev,
// if (folder !== null) { save_path: result,
// setSettings((prev) => ({ }));
// ...prev, sendSettingsToBackend({
// outputFolder: folder, ...settings,
// })); save_path: result,
// } });
}
return null;
});
return null;
}; };
return ( return (
@ -259,13 +266,22 @@ export default function SettingsPage() {
value={settings.save_path} value={settings.save_path}
className="ml-2 w-[300px]" className="ml-2 w-[300px]"
/> />
<button <IconButton
component="label"
size="small"
tabIndex={-1}
onClick={handleFolderChange}
>
<MoreHorizIcon />
</IconButton>
{/* <button
type="button" type="button"
onClick={handleFolderChange} onClick={handleFolderChange}
className="ml-2 px-3 py-1 rounded bg-plumDark text-offwhite hover:bg-plum" className="ml-2 px-3 py-1 rounded bg-plumDark text-offwhite hover:bg-plum"
> >
<VisuallyHiddenInput type="file" />
... ...
</button> </button> */}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,4 +1,4 @@
const getBaseUrl = async () => { export const getBaseUrl = async () => {
const port = await window.audio.getPort(); const port = await window.audio.getPort();
if (port.error || !port.port) { if (port.error || !port.port) {
return `http://localhost:5010`; return `http://localhost:5010`;
@ -7,7 +7,7 @@ const getBaseUrl = async () => {
return `http://localhost:${port.port}`; return `http://localhost:${port.port}`;
}; };
export default async function apiFetch(endpoint: string, options = {}) { export async function apiFetch(endpoint: string, options = {}) {
const url = `${await getBaseUrl()}/${endpoint}`; const url = `${await getBaseUrl()}/${endpoint}`;
return fetch(url, options); return fetch(url, options);
} }

View File

@ -12,10 +12,10 @@ import {
verticalListSortingStrategy, verticalListSortingStrategy,
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import AudioTrimmer from './AudioTrimer'; import AudioTrimmer from './Trimmer/AudioTrimer';
import { ClipMetadata } from '../../redux/types'; import { ClipMetadata } from '../../redux/types';
import { useAppDispatch, useAppSelector } from '../hooks'; import { useAppDispatch, useAppSelector } from '../hooks';
import apiFetch from '../api'; import { apiFetch } from '../api';
export interface ClipListProps { export interface ClipListProps {
collection: string; collection: string;

View File

@ -5,27 +5,22 @@ import React, {
useCallback, useCallback,
useRef, useRef,
} from 'react'; } from 'react';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Slider from '@mui/material/Slider'; import Slider from '@mui/material/Slider';
import ToggleButton from '@mui/material/ToggleButton'; import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { useWavesurfer } from '@wavesurfer/react'; import { useWavesurfer } from '@wavesurfer/react';
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'; import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js'; import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteIcon from '@mui/icons-material/Delete';
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { ClipMetadata, PlaybackType } from '../../redux/types'; import { ClipMetadata, PlaybackType } from '../../../redux/types';
import { useAppSelector } from '../hooks'; import { useAppSelector } from '../../hooks';
import PlayStopIcon from './playStopIcon'; import PlayStopIcon from '../icons/playStopIcon';
import PlayOverlapIcon from './playOverlapIcon'; import PlayOverlapIcon from '../icons/playOverlapIcon';
import NameEditDialog from './dialogs/NameEditDialog';
import DeleteClipDialog from './dialogs/DeleteClipDialog';
import TitleBlock from './TitleBlock';
import ClipButtonRow from './ClipButtonRow';
export interface AudioTrimmerProps { export interface AudioTrimmerProps {
metadata: ClipMetadata; metadata: ClipMetadata;
@ -42,33 +37,23 @@ export default function AudioTrimmer({
}: AudioTrimmerProps) { }: AudioTrimmerProps) {
const { attributes, listeners, setNodeRef, transform, transition } = const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: metadata.filename }); useSortable({ id: metadata.filename });
const rootRef = useRef<HTMLDivElement | null>(null);
// Dialog state for editing name const [isVisible, setIsVisible] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [nameInput, setNameInput] = useState<string>(metadata.name);
const [volumeInput, setVolumeInput] = useState<number>(metadata.volume ?? 1); const [volumeInput, setVolumeInput] = useState<number>(metadata.volume ?? 1);
const collectionNames = useAppSelector((state) => const collectionNames = useAppSelector((state) =>
state.collections.map((col) => col.name), state.collections.map((col) => col.name),
); );
useEffect(() => { const handleDialogSave = (newName: string) => {
setNameInput(metadata.name); if (newName.trim() && newName !== metadata.name) {
}, [metadata.name]); const updated = { ...metadata, name: newName.trim() };
const openEditDialog = () => setEditDialogOpen(true);
const closeEditDialog = () => setEditDialogOpen(false);
const handleDialogSave = () => {
if (nameInput.trim() && nameInput !== metadata.name) {
const updated = { ...metadata, name: nameInput.trim() };
if (onSave) onSave(updated); if (onSave) onSave(updated);
} }
closeEditDialog(); setEditDialogOpen(false);
}; };
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
const containerRef = useRef(null); const containerRef = useRef(null);
// const [clipStart, setClipStart] = useState<number | undefined>(undefined); // const [clipStart, setClipStart] = useState<number | undefined>(undefined);
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined); // const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
@ -83,16 +68,12 @@ export default function AudioTrimmer({
[], [],
); );
const fileBaseName =
metadata.filename.split('\\').pop()?.split('/').pop() || 'Unknown';
const { wavesurfer, isReady, isPlaying } = useWavesurfer({ const { wavesurfer, isReady, isPlaying } = useWavesurfer({
container: containerRef, container: containerRef,
height: 100, height: 100,
waveColor: '#ccb1ff', waveColor: '#ccb1ff',
progressColor: '#6e44ba', progressColor: '#6e44ba',
hideScrollbar: true, hideScrollbar: true,
url: blobUrl,
plugins, plugins,
}); });
@ -202,25 +183,42 @@ export default function AudioTrimmer({
}, [onRegionCreated, onRegionUpdated, plugins]); }, [onRegionCreated, onRegionUpdated, plugins]);
useEffect(() => { useEffect(() => {
let url: string | null = null; const node = rootRef.current;
if (!node) return;
const observer = new window.IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.1 },
);
observer.observe(node);
// eslint-disable-next-line consistent-return
return () => {
observer.disconnect();
};
}, []);
useEffect(() => {
if (!isVisible) return;
let cancelled = false;
async function fetchAudio() { async function fetchAudio() {
// console.log('Loading audio buffer for file:', filename);
const buffer = await window.audio.loadAudioBuffer(metadata.filename); const buffer = await window.audio.loadAudioBuffer(metadata.filename);
// console.log('Received buffer:', buffer.buffer); if (cancelled) return;
if (buffer.buffer && !buffer.error) { if (buffer.buffer && !buffer.error) {
const audioData = buffer.buffer const audioData = buffer.buffer
? new Uint8Array(buffer.buffer) ? new Uint8Array(buffer.buffer)
: buffer; : buffer;
url = URL.createObjectURL(new Blob([audioData])); wavesurfer?.loadBlob(new Blob([audioData]));
// console.log('Created blob URL:', url);
setBlobUrl(url);
} }
} }
fetchAudio(); fetchAudio();
// eslint-disable-next-line consistent-return
return () => { return () => {
if (url) URL.revokeObjectURL(url); cancelled = true;
}; };
}, [metadata.filename]); }, [isVisible, metadata.filename, wavesurfer]);
const onPlayPause = () => { const onPlayPause = () => {
if (wavesurfer === null) return; if (wavesurfer === null) return;
@ -245,7 +243,10 @@ export default function AudioTrimmer({
return ( return (
<div <div
ref={setNodeRef} ref={(el) => {
setNodeRef(el);
rootRef.current = el;
}}
style={{ style={{
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
@ -254,6 +255,21 @@ export default function AudioTrimmer({
}} }}
className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop" className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop"
> >
<NameEditDialog
open={editDialogOpen}
onCancel={() => setEditDialogOpen(false)}
startValue={metadata.name}
onSave={handleDialogSave}
/>
<DeleteClipDialog
open={deleteDialogOpen}
onCancel={() => setDeleteDialogOpen(false)}
onDelete={() => {
setDeleteDialogOpen(false);
if (onDelete) onDelete(metadata);
}}
/>
<div <div
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...attributes} {...attributes}
@ -273,137 +289,22 @@ export default function AudioTrimmer({
{/* <div className="flex flex-col"> */} {/* <div className="flex flex-col"> */}
<div className="ml-4 mr-2 p-2"> <div className="ml-4 mr-2 p-2">
<div className="grid justify-items-stretch grid-cols-2"> <div className="grid justify-items-stretch grid-cols-2">
<div className="mb-5px flex flex-col"> <TitleBlock
<span name={metadata.name}
className="font-bold text-lg text-white mb-1 cursor-pointer" filename={metadata.filename}
onClick={openEditDialog} onNameClick={() => setEditDialogOpen(true)}
onKeyDown={(e) => { />
if (e.key === 'Enter' || e.key === ' ') { <ClipButtonRow
e.preventDefault(); isPlaying={isPlaying}
openEditDialog(); collectionNames={collectionNames}
} onPlayPause={onPlayPause}
}} onMove={(collectionName) => {
title="Click to edit name" if (onMove !== undefined) {
tabIndex={0} onMove(collectionName, metadata);
role="button" }
style={{ outline: 'none' }}
>
{metadata.name}
</span>
<span className="text-sm text-neutral-500">{fileBaseName}</span>
</div>
<Dialog
open={editDialogOpen}
onClose={closeEditDialog}
slotProps={{
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
}} }}
> onDelete={() => setDeleteDialogOpen(true)}
<DialogTitle>Edit Clip Name</DialogTitle> />
<DialogContent>
<textarea
autoFocus
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center resize-y"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
rows={3}
onFocus={(event) => event.target.select()}
aria-label="Edit clip name"
style={{ minHeight: '3em' }}
/>
</DialogContent>
<DialogActions>
<button
type="button"
onClick={closeEditDialog}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Cancel
</button>
<button
type="button"
onClick={handleDialogSave}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Save
</button>
</DialogActions>
</Dialog>
<Dialog
open={deleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
slotProps={{
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
}}
>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>
Are you sure you want to delete this clip?
</DialogContent>
<DialogActions>
<button
type="button"
onClick={() => setDeleteDialogOpen(false)}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Cancel
</button>
<button
type="button"
onClick={() => {
setDeleteDialogOpen(false);
if (onDelete) onDelete(metadataRef.current);
}}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Delete
</button>
</DialogActions>
</Dialog>
<div className="flex justify-end">
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={onPlayPause}
>
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
</button>
<div className="relative inline-block">
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={() => setDropdownOpen((prev) => !prev)}
>
{dropdownOpen ? <ArrowDownwardIcon /> : <ArrowForwardIcon />}
</button>
{dropdownOpen && (
<div className="absolute z-10 mt-2 w-40 bg-midnight rounded-md shadow-lg">
{collectionNames.map((name) => (
<button
key={name}
type="button"
className="block w-full text-left px-4 py-2 text-white hover:bg-plumDark"
onClick={() => {
setDropdownOpen(false);
if (onMove) onMove(name, metadata);
}}
>
{name}
</button>
))}
</div>
)}
</div>
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={() => setDeleteDialogOpen(true)}
>
<DeleteIcon />
</button>
</div>
</div> </div>
<div className="m-1 wavesurfer-scroll-container"> <div className="m-1 wavesurfer-scroll-container">
<div ref={containerRef} className="wavesurfer-inner" /> <div ref={containerRef} className="wavesurfer-inner" />
@ -428,24 +329,6 @@ export default function AudioTrimmer({
color="secondary" color="secondary"
className="p-0 m-0" className="p-0 m-0"
/> />
{/* <input
type="range"
min={0}
max={1}
step={0.01}
value={volumeInput}
onChange={(e) => {
const newVolume = parseFloat(e.target.value);
setVolumeInput(newVolume);
}}
onDragEnd={(e) => {
console.log('Volume change:');
// const newVolume = parseFloat(e.target.value);
// if (onSave) onSave({ ...metadata, volume: newVolume });
}}
className="mx-2 w-full accent-plum"
aria-label="Volume slider"
/> */}
</div> </div>
<div className="w-1/5 flex justify-end text-sm text-neutral-500"> <div className="w-1/5 flex justify-end text-sm text-neutral-500">
<ToggleButtonGroup value={metadata.playbackType}> <ToggleButtonGroup value={metadata.playbackType}>

View File

@ -0,0 +1,67 @@
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteIcon from '@mui/icons-material/Delete';
import { useState } from 'react';
export default function ClipButtonRow({
isPlaying,
collectionNames,
onPlayPause,
onMove,
onDelete,
}: {
isPlaying: boolean;
collectionNames: string[];
onPlayPause: () => void;
onMove?: (collectionName: string) => void;
onDelete?: () => void;
}) {
const [dropdownOpen, setDropdownOpen] = useState(false);
return (
<div className="flex justify-end">
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={onPlayPause}
>
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
</button>
<div className="relative inline-block">
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={() => setDropdownOpen((prev) => !prev)}
>
{dropdownOpen ? <ArrowDownwardIcon /> : <ArrowForwardIcon />}
</button>
{dropdownOpen && (
<div className="absolute z-10 mt-2 w-40 bg-midnight rounded-md shadow-lg">
{collectionNames.map((name) => (
<button
key={name}
type="button"
className="block w-full text-left px-4 py-2 text-white hover:bg-plumDark"
onClick={() => {
setDropdownOpen(false);
if (onMove) onMove(name);
}}
>
{name}
</button>
))}
</div>
)}
</div>
<button
type="button"
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
onClick={() => onDelete && onDelete()}
>
<DeleteIcon />
</button>
</div>
);
}

View File

@ -0,0 +1,27 @@
export default function TitleBlock({
name,
filename,
onNameClick,
}: {
name: string;
filename: string;
onNameClick: () => void;
}) {
const basename = filename.split('\\').pop()?.split('/').pop() || 'Unknown';
return (
<div className="mb-5px flex flex-col">
<span
className="font-bold text-lg text-white mb-1 cursor-pointer"
onClick={onNameClick}
onKeyDown={(e) => {}}
title="Click to edit name"
tabIndex={0}
role="button"
style={{ outline: 'none' }}
>
{name}
</span>
<span className="text-sm text-neutral-500">{basename}</span>
</div>
);
}

View File

@ -0,0 +1,46 @@
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '@mui/material';
export default function DeleteClipDialog({
open,
onCancel,
onDelete,
}: {
open: boolean;
onCancel: () => void;
onDelete: () => void;
}) {
return (
<Dialog
open={open}
onClose={onCancel}
slotProps={{
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
}}
>
<DialogTitle>Confirm Delete</DialogTitle>
<DialogContent>Are you sure you want to delete this clip?</DialogContent>
<DialogActions>
<button
type="button"
onClick={onCancel}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Cancel
</button>
<button
type="button"
onClick={onDelete}
autoFocus
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Delete
</button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,70 @@
import { useEffect, useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
} from '@mui/material';
export default function NameEditDialog({
open,
startValue,
onCancel,
onSave,
}: {
open: boolean;
startValue: string;
onCancel: () => void;
onSave: (newName: string) => void;
}) {
const [input, setInput] = useState(startValue);
useEffect(() => {
if (open) {
setInput(startValue);
}
}, [open, startValue]);
return (
<Dialog
open={open}
onClose={onCancel}
slotProps={{
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
}}
>
<DialogTitle>Edit Clip Name</DialogTitle>
<DialogContent>
<TextField
autoFocus
multiline
variant="standard"
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center resize-y"
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
onFocus={(event) => event.target.select()}
aria-label="Edit clip name"
/>
</DialogContent>
<DialogActions>
<button
type="button"
onClick={onCancel}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Cancel
</button>
<button
type="button"
onClick={() => onSave(input)}
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Save
</button>
</DialogActions>
</Dialog>
);
}

View File

@ -6,7 +6,7 @@
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'" content="script-src 'self' 'unsafe-inline'"
/> />
<title>Hello Electron React!</title> <title>ClipTrim</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

421
electron-ui/test_meta.json Normal file
View File

@ -0,0 +1,421 @@
[
{
"name": "Uncategorized",
"id": 0,
"clips": []
},
{
"name": "mason",
"id": 1,
"clips": [
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250105_131700.wav",
"name": "lich",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 27.371372936207585,
"endTime": 30
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250119_173448.wav",
"name": "nic",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.897134459955918,
"endTime": 10.62821454812639
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250119_173654.wav",
"name": "racist",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 7.92372881355932,
"endTime": 9.682203389830498
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250120_210843.wav",
"name": "dildo",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.227565151875025,
"endTime": 13.20035827476919
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250121_223502.wav",
"name": "latter",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.440677966101688,
"endTime": 12.499999999999996
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250124_214433.wav",
"name": "ahh",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10,
"endTime": 10.656779661016953
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250131_204903.wav",
"name": "tight",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.7457627118644,
"endTime": 11.52542372881357
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250308_104030.wav",
"name": "rape",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 3.7923728813559365,
"endTime": 5.677966101694913
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250328_212948.wav",
"name": "wig",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.31779661016946,
"endTime": 11.038135593220328
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250419_140818.wav",
"name": "queef",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 12.47881355932203,
"endTime": 13.347457627118642
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250503_183629.wav",
"name": "wood",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.82627118644066,
"endTime": 11.546610169491522
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250707_213558.wav",
"name": "bam",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 13.728813559321997,
"endTime": 14.300847457627134
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250707_222904.wav",
"name": "uhh",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 13.199152542372879,
"endTime": 14.830508474576275
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250808_194926.wav",
"name": "rights",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.131355932203387,
"endTime": 10.69915254237289
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250809_193435.wav",
"name": "u r wet",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.983050847457612,
"endTime": 10.14830508474577
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250809_222039.wav",
"name": "run",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 4.216101694915256,
"endTime": 11.038135593220332
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250814_215842.wav",
"name": "suprise",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.927966101694913,
"endTime": 14.300847457627116
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250920_174822.wav",
"name": "my",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 5.736975857687425,
"endTime": 6.202880135535784
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250920_174950.wav",
"name": "whatsup",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 2.870606674248936,
"endTime": 3.3193015062831197
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251018_211620.wav",
"name": "cheeks",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.338983050847464,
"endTime": 12.394067796610184
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251031_211310.wav",
"name": "michal",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.516949152542374,
"endTime": 12.415254237288133
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251107_222607.wav",
"name": "blegh",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.216101694915253,
"endTime": 9.957627118644073
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251115_201357.wav",
"name": "bohemian",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.618644067796604,
"endTime": 11.274508356463695
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251213_114932.wav",
"name": "electro",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.915254237288137,
"endTime": 13.771186440677946
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20260201_111049.wav",
"name": "nword",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.834745762711867,
"endTime": 10.911016949152565
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20260206_230124.wav",
"name": "fist",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 12.458333333333336,
"endTime": 13.708333333333327
}
]
},
{
"name": "jake",
"id": 2,
"clips": [
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250117_194006.wav",
"name": "do it",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 14.152542372881365,
"endTime": 14.936440677966102
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250121_223258.wav",
"name": "cooch",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.538135593220337,
"endTime": 10.656779661016952
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250215_214039.wav",
"name": "domestic",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.40254237288135,
"endTime": 13.05084745762703
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250412_134821.wav",
"name": "god",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.927966101694915,
"endTime": 13.834745762711863
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250606_212121.wav",
"name": "poop\nmyself",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 7.881355932203395,
"endTime": 12.055084745762716
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250719_120451.wav",
"name": "tasmania",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.038135593220334,
"endTime": 13.686440677966088
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250822_205916.wav",
"name": "jews",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 11.122881355932197,
"endTime": 12.097457627118638
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251026_211500.wav",
"name": "terror",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.572033898305074,
"endTime": 11.588983050847439
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251108_170721.wav",
"name": "toon\ntown",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 5.148305084745765,
"endTime": 8.411016949152545
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20260103_222442.wav",
"name": "whooping",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 7.309322033898307,
"endTime": 9.046610169491542
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20260107_210712.wav",
"name": "no head",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.050847457627118,
"endTime": 9.279661016949134
}
]
},
{
"name": "isaac",
"id": 3,
"clips": [
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250121_203752.wav",
"name": "blow",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.800018422991895,
"endTime": 11.453804347826084
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250125_002323.wav",
"name": "frying",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.337093249867106,
"endTime": 11.49862694147519
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250125_230923.wav",
"name": "cum",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.728813559322031,
"endTime": 9.894067796610173
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250126_131833.wav",
"name": "liquid",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.487288135593221,
"endTime": 11.86440677966102
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250131_220452.wav",
"name": "nuts",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.135593220338984,
"endTime": 8.983050847457633
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251101_205146.wav",
"name": "hard",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.283898305084744,
"endTime": 10.720338983050835
}
]
},
{
"name": "nat",
"id": 4,
"clips": [
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250125_171754.wav",
"name": "hot dog",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 8.644067796610168,
"endTime": 11.05932203389828
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250131_212540.wav",
"name": "plink",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 10.40254237288135,
"endTime": 12.012711864406779
}
]
},
{
"name": "misc",
"id": 5,
"clips": [
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250223_110900.wav",
"name": "bounce",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 2.521186440677966,
"endTime": 7.4152542372881225
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250228_221700.wav",
"name": "avada",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 1.8220338983050826,
"endTime": 5.338983050847453
},
{
"filename": "C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20251212_192830.wav",
"name": "sandler",
"playbackType": "playOverlap",
"volume": 1,
"startTime": 9.576271186440678,
"endTime": 12.394067796610187
}
]
}
]

View File

@ -1,21 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup> </startup>
<runtime> <runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral"/> <assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.6.0.0" newVersion="2.6.0.0"/> <bindingRedirect oldVersion="0.0.0.0-2.6.0.0" newVersion="2.6.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/> <assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/> <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0"/> <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly> </dependentAssembly>
</assemblyBinding> </assemblyBinding>
</runtime> </runtime>

View File

@ -1,10 +1,13 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text.Json.Serialization;
namespace ClipTrimDotNet.Client namespace ClipTrimDotNet.Client
{ {
@ -16,27 +19,35 @@ namespace ClipTrimDotNet.Client
public class ClipMetadata public class ClipMetadata
{ {
[JsonProperty(PropertyName = "filename")] [JsonProperty(PropertyName = "filename")]
[JsonPropertyName("filename")]
public string Filename { get; set; } public string Filename { get; set; }
[JsonProperty(PropertyName = "name")] [JsonProperty(PropertyName = "name")]
[JsonPropertyName("name")]
public string Name { get; set; } public string Name { get; set; }
[JsonProperty(PropertyName = "volume")] [JsonProperty(PropertyName = "volume")]
[JsonPropertyName("volume")]
public double Volume { get; set; } = 1.0; public double Volume { get; set; } = 1.0;
[JsonProperty(PropertyName = "startTime")] [JsonProperty(PropertyName = "startTime")]
[JsonPropertyName("startTime")]
public double StartTime { get; set; } = 0.0; public double StartTime { get; set; } = 0.0;
[JsonProperty(PropertyName = "endTime")] [JsonProperty(PropertyName = "endTime")]
[JsonPropertyName("endTime")]
public double EndTime { get; set; } = 0.0; public double EndTime { get; set; } = 0.0;
[JsonProperty(PropertyName = "playbackType")] [JsonProperty(PropertyName = "playbackType")]
[JsonConverter(typeof(StringEnumConverter))] [Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]
[JsonPropertyName("playbackType")]
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop; public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
} }
} }

View File

@ -1,10 +1,16 @@
using System; using BarRaider.SdTools;
using ClipTrimDotNet.Keys;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Newtonsoft.Json;
using SocketIOClient;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using static System.Runtime.InteropServices.JavaScript.JSType;
namespace ClipTrimDotNet.Client namespace ClipTrimDotNet.Client
{ {
@ -23,87 +29,258 @@ namespace ClipTrimDotNet.Client
} }
} }
private HttpClient httpClient; //private HttpClient httpClient;
private SocketIO? socket;
public string HostName
{
get
{
//return $"http://localhost:5010/";
return $"http://localhost:{GlobalSettings.Instance.PortNumber}/";
}
}
private string? currentHostname = null;
void CreateSocket()
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"Starting ClipTrimClient on port {HostName}");
socket = new SocketIO(new Uri(HostName));
currentHostname = HostName;
socket.Options.AutoUpgrade = false;
//socket.Options.Path = "/socket.io";
socket.Options.ConnectionTimeout = TimeSpan.FromSeconds(10);
socket.Options.Reconnection = true;
socket.On("full_data", ctx =>
{
try
{
var response = ctx.GetValue<List<CollectionMetaData>>(0);
Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data event {JsonConvert.SerializeObject(response)}");
Collections = response!;
Player.TickAll();
PageNavigator.TickAll();
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Collections {JsonConvert.SerializeObject(Collections)}");
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data error {ex.ToString()}");
}
return Task.CompletedTask;
});
socket.On("collection_updated", ctx =>
{
try
{
var response = ctx.GetValue<CollectionMetaData>(0)!;
Logger.Instance.LogMessage(TracingLevel.INFO, $"collection_updated event {JsonConvert.SerializeObject(response)}");
int index = Collections.FindIndex(x => x.Id == response.Id);
if (index != -1)
{
Collections[index] = response;
Player.TickAll();
PageNavigator.TickAll();
}
}
catch
{
}
return Task.CompletedTask;
});
socket.OnConnected += (sender, e) =>
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket connected: {e}");
};
socket.OnDisconnected += (sender, e) =>
{
Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket disconnected: {e}");
Task.Run(async () => await Connect());
};
Task.Run(async () => await Connect());
}
public ClipTrimClient() public ClipTrimClient()
{ {
httpClient = new HttpClient() //httpClient = new HttpClient()
{ //{
BaseAddress = new Uri("http://localhost:5010/"), // BaseAddress = new Uri("http://localhost:5010/"),
Timeout = TimeSpan.FromSeconds(10) // Timeout = TimeSpan.FromSeconds(10)
}; //};
Task.Run(ShortPoll); CreateSocket();
} }
public async Task ShortPoll() public async Task Connect()
{ {
while (true) if (socket is null) return;
while (!socket.Connected)
{ {
await GetMetadata(); try
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5)); {
await socket.ConnectAsync();
}
catch
{
}
} }
} }
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>(); public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
public CollectionMetaData? SelectedCollection { get; private set; } public int SelectedCollection { get; private set; } = -1;
public int PageIndex { get; private set; } = 0;
private async Task GetMetadata()
{
try
{
var response = await httpClient.GetAsync("meta");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
dynamic collections = JsonConvert.DeserializeObject(json);
collections = collections.collections;
Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString());
}
}
catch (Exception ex)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}");
return;
}
public Dictionary<int, int> CollectionIndexes { get; private set; } = new();
public int PageIndex
{
get
{
if (SelectedCollection == -1) return 0;
if (!CollectionIndexes.ContainsKey(SelectedCollection))
{
CollectionIndexes[SelectedCollection] = 0;
}
return CollectionIndexes[SelectedCollection];
}
set
{
if (SelectedCollection == -1) return;
CollectionIndexes[SelectedCollection] = value;
}
}
public bool PageMode { get; set; } = false;
public int PageCount
{
get
{
if (SelectedCollection == -1) return 0;
var collection = Collections[SelectedCollection];
return (collection.Clips.Count - 1) / 10 + 1;
}
}
public bool CanPageUp
{
get
{
if(PageMode) return false;
if (SelectedCollection == -1) return false;
return PageCount - PageIndex > 1;
}
} }
public bool CanPageDown
{
get
{
if (PageMode) return false;
return PageIndex > 0;
}
}
public void PageDown()
{
if (CanPageDown)
{
PageIndex--;
}
}
public void PageUp()
{
if (CanPageUp)
{
PageIndex++;
}
}
public List<string> GetCollectionNames() public List<string> GetCollectionNames()
{ {
//await GetMetadata(); //await GetMetadata();
return Collections.Select(x => x.Name).ToList(); return Collections.Where(x => x.Name != "Uncategorized").Select(x => x.Name).ToList();
} }
public void SetSelectedCollectionByName(string name) public string GetCurrentCollectionName()
{ {
var collection = Collections.FirstOrDefault(x => x.Name == name); if (SelectedCollection == -1) return "";
if (collection != null) return Collections[SelectedCollection].Name;
}
public string GetPlayerStringByCoordinateIndex(int index)
{
if (PageMode)
{ {
SelectedCollection = collection; int pageNumber = index + 1;
PageIndex = 0; if(pageNumber <= PageCount)
{
return pageNumber.ToString();
}
return "";
}
else
{
var collection = GetClipByPagedIndex(index);
return collection?.Name ?? "";
} }
} }
public ClipMetadata? GetClipByPagedIndex(int index) public ClipMetadata? GetClipByPagedIndex(int index)
{ {
if (SelectedCollection == null) return null; SelectedCollection = Collections.FindIndex(x => x.Name == GlobalSettings.Instance.ProfileName);
if (SelectedCollection == -1) return null;
int clipIndex = PageIndex * 10 + index; int clipIndex = PageIndex * 10 + index;
if (clipIndex >= 0 && clipIndex < SelectedCollection.Clips.Count) var collection = Collections[SelectedCollection];
if (clipIndex >= 0 && clipIndex < collection.Clips.Count)
{ {
return SelectedCollection.Clips[clipIndex]; return collection.Clips[clipIndex];
} }
return null; return null;
} }
public async void PlayClip(ClipMetadata? metadata) public async void PlayClip(int index)
{ {
if (metadata == null) return; if (PageMode)
var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json"));
if (!response.IsSuccessStatusCode)
{ {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}"); if(index < 0 || index >= PageCount) return;
PageIndex = index;
PageMode = false;
Player.TickAll();
PageNavigator.TickAll();
}
else
{
if (socket is null) return;
var metadata = GetClipByPagedIndex(index);
if (metadata == null) return;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"playing clip:");
await socket.EmitAsync("play_clip", new List<object>() { metadata });
}
}
public async void SaveClip()
{
if (socket is null) return;
await socket.EmitAsync("record_clip", new List<object>() { });
}
public async void CheckPort()
{
if (socket is null) return;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Checking port {socket}");
if (currentHostname != HostName)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"port {socket}");
if (socket.Connected)
{
await socket.DisconnectAsync();
}
socket.Dispose();
CreateSocket();
} }
} }
} }

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ClipTrimDotNet.Client namespace ClipTrimDotNet.Client
@ -10,14 +11,17 @@ namespace ClipTrimDotNet.Client
public class CollectionMetaData public class CollectionMetaData
{ {
[JsonProperty(PropertyName = "name")] [JsonProperty(PropertyName = "name")]
[JsonPropertyName("name")]
public string Name { get; set; } public string Name { get; set; }
[JsonProperty(PropertyName = "clips")] [JsonProperty(PropertyName = "clips")]
[JsonPropertyName("clips")]
public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>(); public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
[JsonProperty(PropertyName = "id")] [JsonProperty(PropertyName = "id")]
[JsonPropertyName("id")]
public int Id { get; set; } public int Id { get; set; }
} }
} }

View File

@ -1,147 +1,110 @@
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <TargetFramework>net8.0-windows</TargetFramework>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProjectGuid>{4635D874-69C0-4010-BE46-77EF92EB1553}</ProjectGuid>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<RootNamespace>ClipTrimDotNet</RootNamespace> <LangVersion>10</LangVersion>
<AssemblyName>ClipTrimDotNet</AssemblyName> <Nullable>enable</Nullable>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>8</LangVersion> <PreBuildEvent>npm run stop</PreBuildEvent>
<FileAlignment>512</FileAlignment> <PostBuildEvent>npm run start</PostBuildEvent>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AssemblyTitle>ClipTrimDotNet</AssemblyTitle>
<Deterministic>true</Deterministic> <Product>ClipTrimDotNet</Product>
<Nullable>enable</Nullable> <Copyright>Copyright © 2020</Copyright>
<TargetFrameworkProfile /> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath> <OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath> <OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="CommandLine, Version=2.9.1.0, Culture=neutral, PublicKeyToken=5a870481e358d379, processorArchitecture=MSIL"> <None Remove="Images\app_icon.png" />
<HintPath>..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll</HintPath> <None Remove="Images\back.png" />
</Reference> <None Remove="Images\category_icon.png" />
<Reference Include="Microsoft.Win32.Registry, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <None Remove="Images\collection.png" />
<HintPath>..\packages\Microsoft.Win32.Registry.4.7.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath> <None Remove="Images\collection_icon.png" />
</Reference> <None Remove="Images\page_nav.png" />
<Reference Include="NAudio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <None Remove="Images\page_nav_icon.png" />
<HintPath>..\packages\NAudio.2.2.1\lib\net472\NAudio.dll</HintPath> <None Remove="Images\player.png" />
</Reference> <None Remove="Images\player_icon.png" />
<Reference Include="NAudio.Asio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <None Remove="Images\record.png" />
<HintPath>..\packages\NAudio.Asio.2.2.1\lib\netstandard2.0\NAudio.Asio.dll</HintPath> <None Remove="Images\record_icon.png" />
</Reference> <None Remove="manifest.json" />
<Reference Include="NAudio.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
<HintPath>..\packages\NAudio.Core.2.2.1\lib\netstandard2.0\NAudio.Core.dll</HintPath>
</Reference>
<Reference Include="NAudio.Midi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
<HintPath>..\packages\NAudio.Midi.2.2.1\lib\netstandard2.0\NAudio.Midi.dll</HintPath>
</Reference>
<Reference Include="NAudio.Wasapi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
<HintPath>..\packages\NAudio.Wasapi.2.2.1\lib\netstandard2.0\NAudio.Wasapi.dll</HintPath>
</Reference>
<Reference Include="NAudio.WinForms, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
<HintPath>..\packages\NAudio.WinForms.2.2.1\lib\net472\NAudio.WinForms.dll</HintPath>
</Reference>
<Reference Include="NAudio.WinMM, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
<HintPath>..\packages\NAudio.WinMM.2.2.1\lib\netstandard2.0\NAudio.WinMM.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NLog, Version=6.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
<HintPath>..\packages\NLog.6.0.5\lib\net46\NLog.dll</HintPath>
</Reference>
<Reference Include="StreamDeckTools, Version=6.3.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\StreamDeck-Tools.6.3.2\lib\netstandard2.0\StreamDeckTools.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Drawing.Common, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.Drawing.Common.9.0.10\lib\net462\System.Drawing.Common.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Security.AccessControl, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.AccessControl.4.7.0\lib\net461\System.Security.AccessControl.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal.Windows, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\packages\System.Security.Principal.Windows.4.7.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel" />
<Reference Include="System.Transactions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="BaseTest.cs" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<Compile Include="Client\ClipMetadata.cs" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<Compile Include="Client\ClipTrimClient.cs" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<Compile Include="Client\CollectionMetaData.cs" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<Compile Include="GlobalSettings.cs" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<Compile Include="Player.cs" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<Compile Include="ProfileSwitcher.cs" /> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
<Compile Include="Program.cs" /> <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.2" />
<Compile Include="Properties\AssemblyInfo.cs" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<Compile Include="WavPlayer.cs" /> <PackageReference Include="NLog" Version="6.0.5" />
<PackageReference Include="SocketIOClient" Version="4.0.0.2" />
<PackageReference Include="SocketIOClient.Common" Version="4.0.0" />
<PackageReference Include="SocketIOClient.Serializer" Version="4.0.0.1" />
<PackageReference Include="SocketIOClient.Serializer.NewtonsoftJson" Version="4.0.0.1" />
<PackageReference Include="StreamDeck-Tools" Version="6.3.2" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
<PackageReference Include="System.Drawing.Common" Version="9.0.10" />
<PackageReference Include="System.IO.Pipelines" Version="10.0.2" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.2" />
<PackageReference Include="System.Text.Json" Version="10.0.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
<PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
<PackageReference Include="CoreWCF.ConfigurationManager" Version="1.8.0" />
<PackageReference Include="CoreWCF.Http" Version="1.8.0" />
<PackageReference Include="CoreWCF.WebHttp" Version="1.8.0" />
<PackageReference Include="CoreWCF.NetTcp" Version="1.8.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Update="DialLayout.json">
<None Include="DialLayout.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Include="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="!!README!!.txt" /> <Content Include="!!README!!.txt" />
<Content Include="Images\categoryIcon%402x.png"> <Content Include="Images\app_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\categoryIcon.png"> <Content Include="Images\back.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\icon%402x.png"> <Content Include="Images\category_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\icon.png"> <Content Include="Images\collection.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginAction%402x.png"> <Content Include="Images\collection_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginAction.png"> <Content Include="Images\page_nav.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginIcon%402x.png"> <Content Include="Images\page_nav_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginIcon.png"> <Content Include="Images\player_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\record.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\player.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\record_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="package.json" /> <Content Include="package.json" />
@ -152,11 +115,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>npm run stop</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>npm run start</PostBuildEvent>
</PropertyGroup>
</Project> </Project>

View File

@ -7,32 +7,9 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BarRaider.SdTools.Wrappers; using BarRaider.SdTools.Wrappers;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NAudio.MediaFoundation;
namespace ClipTrimDotNet namespace ClipTrimDotNet
{ {
public class FileEntry
{
public FileEntry()
{
Volume = 1.0;
Playtype = "Play/Overlap";
}
[JsonProperty(PropertyName = "Volume")]
public double Volume { get; set; }
[JsonProperty(PropertyName = "Playtype")]
public string Playtype { get; set; }
}
public class CollectionEntry
{
public CollectionEntry()
{
Files = new Dictionary<string, FileEntry>();
}
[JsonProperty(PropertyName = "Files")]
public Dictionary<string, FileEntry> Files { get; set; }
}
public class GlobalSettings public class GlobalSettings
{ {
public static GlobalSettings? _inst; public static GlobalSettings? _inst;
@ -51,59 +28,22 @@ namespace ClipTrimDotNet
public static GlobalSettings CreateDefaultSettings() public static GlobalSettings CreateDefaultSettings()
{ {
GlobalSettings instance = new GlobalSettings(); GlobalSettings instance = new GlobalSettings();
instance.BasePath = null;
instance.ProfileName = null; instance.ProfileName = null;
instance.Collections = new Dictionary<string, CollectionEntry>(); instance.PortNumber = 5010;
return instance; return instance;
} }
[FilenameProperty]
[JsonProperty(PropertyName = "basePath")]
public string? BasePath { get; set; }
[JsonProperty(PropertyName = "profileName")] [JsonProperty(PropertyName = "profileName")]
public string? ProfileName { get; set; } public string? ProfileName { get; set; }
[JsonProperty(PropertyName = "outputDevice")] [JsonProperty(PropertyName = "portNumber")]
public string? OutputDevice { get; set; } public int? PortNumber { get; set; }
[JsonProperty(PropertyName = "collections")]
public Dictionary<string, CollectionEntry> Collections { get; set; }
public void SetCurrentProfile(string profile) public void SetCurrentProfile(string profile)
{ {
ProfileName = profile; ProfileName = profile;
if(!Collections.ContainsKey(profile))
{
Collections.Add(profile, new CollectionEntry());
}
}
public FileEntry GetFileOptionsInCurrentProfile(string filename)
{
if(!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
{
return new FileEntry();
}
if(!collection.Files.TryGetValue(filename, out FileEntry file))
{
return new FileEntry();
}
Logger.Instance.LogMessage(TracingLevel.INFO, "fetched file settings " + filename + JsonConvert.SerializeObject(file));
return file;
}
public void SetFileOptionsCurrentProfile(string filename, FileEntry file)
{
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile ");
if (!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
{
return;
}
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile 2");
//collection.Files[filename] = file;
Collections[ProfileName].Files[filename] = file;
} }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,49 @@
using BarRaider.SdTools;
using ClipTrimDotNet.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
[PluginActionId("com.michal-courson.cliptrim.clip-save")]
public class ClipSave : KeypadBase
{
public ClipSave(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
GlobalSettingsManager.Instance.RequestGlobalSettings();
}
public void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
ClipTrimClient.Instance.SaveClip();
}
public override void OnTick()
{
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
}
}
}

View File

@ -0,0 +1,107 @@
using BarRaider.SdTools;
using ClipTrimDotNet.Client;
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
[PluginActionId("com.michal-courson.cliptrim.page-navigator")]
public class PageNavigator : KeypadBase
{
static List<PageNavigator> instances = new();
private KeyCoordinates coordinates;
public PageNavigator(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
coordinates = payload.Coordinates;
GlobalSettingsManager.Instance.RequestGlobalSettings();
instances.Add(this);
OnTick();
}
public void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
}
public int GetIndex()
{
int index = Math.Min(Math.Max(coordinates.Column - 1, 0), 2);
return index;
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
switch (GetIndex())
{
case 0:
ClipTrimClient.Instance.PageDown();
break;
case 1:
ClipTrimClient.Instance.PageMode = !ClipTrimClient.Instance.PageMode;
break;
case 2:
ClipTrimClient.Instance.PageUp();
break;
}
Player.TickAll();
TickAll();
}
public static void TickAll()
{
foreach (var instance in instances)
{
instance.OnTick();
}
instances.RemoveAll(i => i == null);
}
private async void SetTitle()
{
switch (GetIndex())
{
case 0:
await Connection.SetTitleAsync(ClipTrimClient.Instance.CanPageDown ? "<" : "");
break;
case 1:
await Connection.SetTitleAsync(ClipTrimClient.Instance.GetCurrentCollectionName() + "\n" + (ClipTrimClient.Instance.PageIndex + 1).ToString());
break;
case 2:
await Connection.SetTitleAsync(ClipTrimClient.Instance.CanPageUp ? ">" : "");
break;
}
}
public override void OnTick()
{
SetTitle();
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
//if (payload.Settings == null || payload.Settings.Count == 0)
//{
// var inst = GlobalSettings.Instance;
// //GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
//}
//else
//{
// GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
//}
}
}
}

View File

@ -0,0 +1,87 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
using System.Text.Json.Serialization;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
[PluginActionId("com.michal-courson.cliptrim.player")]
public class Player : KeypadBase
{
private int coordIndex;
private string? current_text = null;
private KeyCoordinates coordinates;
static List<Player> instances = new();
public Player(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
coordinates = payload.Coordinates;
GlobalSettingsManager.Instance.RequestGlobalSettings();
instances.Add(this);
coordIndex = Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0);
OnTick();
}
public static void TickAll()
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Ticking all Player instances" + instances.Count);
foreach (var instance in instances)
{
instance.OnTick();
}
instances.RemoveAll(i => i == null);
}
private async void CheckFile()
{
var next_text = ClipTrimClient.Instance.GetPlayerStringByCoordinateIndex(coordIndex);
if(next_text != current_text)
{
current_text = next_text;
await Connection.SetTitleAsync($"{current_text ?? ""}");
}
current_text = next_text;
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
ClipTrimClient.Instance.PlayClip(coordIndex);
}
public override void OnTick() {
CheckFile();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
}
}

Some files were not shown because too many files have changed in this diff Show More