Compare commits
31 Commits
plugin_mig
...
5e50b29625
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e50b29625 | |||
| d37cd773f8 | |||
| 801966e8d8 | |||
| 39395fd846 | |||
| 510b92f669 | |||
| aefb3f2648 | |||
| a613b26ba8 | |||
| 7a471041e7 | |||
| ab57d8ef22 | |||
| 69c9d80a82 | |||
| 8c83819a17 | |||
| ad07bf7fe6 | |||
| bc40f9abe3 | |||
| e7f649ae0b | |||
| e34903b20f | |||
| 192c959d39 | |||
| 60123d7450 | |||
| 8265951bd4 | |||
| 757d5ef1a7 | |||
| d78c49d0ad | |||
| 089023e7cf | |||
| db97747f2e | |||
| 1e7141c43f | |||
| 8fda2a03af | |||
| 47cdaa76b6 | |||
| d49ac95fa2 | |||
| f2718282c7 | |||
| 86e30e6ec3 | |||
| b8f26496a0 | |||
| a761b81dd1 | |||
| c1948182ec |
70
.github/upgrades/dotnet-upgrade-plan.md
vendored
Normal 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
|
||||||
4
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"idf.pythonInstallPath": "C:\\Users\\mickl\\.espressif\\tools\\idf-python\\3.11.2\\python.exe"
|
"idf.pythonInstallPath": "C:\\Users\\mickl\\.espressif\\tools\\idf-python\\3.11.2\\python.exe"
|
||||||
}
|
}
|
||||||
3
audio-service/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
recordings/
|
recordings/
|
||||||
|
__pycache__/
|
||||||
@ -1,34 +1,24 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"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": "Test",
|
"name": "Clip 20260228_210924",
|
||||||
"id": 1,
|
"playbackType": "playStop",
|
||||||
"clips": []
|
"volume": 1.0
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
"name": "New",
|
},
|
||||||
"id": 2,
|
{
|
||||||
"clips": [
|
"name": "Test",
|
||||||
{
|
"id": 1,
|
||||||
"endTime": 30,
|
"clips": []
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav",
|
},
|
||||||
"name": "Pee pee\npoo poo",
|
{
|
||||||
"playbackType": "playStop",
|
"name": "New",
|
||||||
"startTime": 27.756510985786615,
|
"id": 2,
|
||||||
"volume": 1
|
"clips": []
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"endTime": 28.597210828548004,
|
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
|
|
||||||
"name": "Clip 20260220_200442",
|
|
||||||
"playbackType": "playStop",
|
|
||||||
"startTime": 26.1853978671042,
|
|
||||||
"volume": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
@ -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
|
|
||||||
|
|||||||
@ -1,10 +1,17 @@
|
|||||||
{
|
{
|
||||||
"input_device": {
|
"input_device": {
|
||||||
"default_samplerate": 44100.0,
|
"channels": 2,
|
||||||
"index": 1,
|
"default_samplerate": 48000,
|
||||||
"max_input_channels": 8,
|
"index": 55,
|
||||||
"name": "VM Mic mix (VB-Audio Voicemeete"
|
"name": "VM Mic mix (VB-Audio Voicemeeter VAIO)"
|
||||||
},
|
},
|
||||||
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings",
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings",
|
||||||
"recording_length": 30
|
"recording_length": 30,
|
||||||
|
"output_device": {
|
||||||
|
"channels": 2,
|
||||||
|
"default_samplerate": 48000,
|
||||||
|
"index": 45,
|
||||||
|
"name": "VM to Discord (VB-Audio Voicemeeter VAIO)"
|
||||||
|
},
|
||||||
|
"http_port": 5010
|
||||||
}
|
}
|
||||||
64
audio-service/src/audio_clip.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import scipy.signal
|
||||||
|
import scipy.io.wavfile as wavfile
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
class AudioClip:
|
||||||
|
def __init__(self, metadata, target_sample_rate=44100):
|
||||||
|
"""
|
||||||
|
metadata: dict with keys 'filename', 'start', 'end' (seconds)
|
||||||
|
target_sample_rate: sample rate for playback
|
||||||
|
"""
|
||||||
|
self.metadata = metadata
|
||||||
|
self.file_path = metadata['filename']
|
||||||
|
self.start = metadata.get('startTime', 0)
|
||||||
|
self.end = metadata.get('endTime', None)
|
||||||
|
self.target_sample_rate = target_sample_rate
|
||||||
|
self.volume = metadata.get('volume', 1.0)
|
||||||
|
self.finished = False
|
||||||
|
self.audio_data, self.sample_rate = self._load_and_process_audio()
|
||||||
|
print(f"AudioClip created for {self.file_path} with start={self.start}s, end={self.end}s, sample_rate={self.sample_rate}Hz, length={len(self.audio_data)/self.sample_rate:.2f}s")
|
||||||
|
self.position = 0 # sample index for playback
|
||||||
|
|
||||||
|
def _load_and_process_audio(self):
|
||||||
|
# Load audio file
|
||||||
|
sample_rate, data = wavfile.read(self.file_path)
|
||||||
|
# Convert to float32
|
||||||
|
if data.dtype != np.float32:
|
||||||
|
data = data.astype(np.float32) / np.max(np.abs(data))
|
||||||
|
# Convert to mono if needed
|
||||||
|
if len(data.shape) > 1:
|
||||||
|
data = np.mean(data, axis=1)
|
||||||
|
# Resample if needed
|
||||||
|
if sample_rate != self.target_sample_rate:
|
||||||
|
num_samples = int(len(data) * self.target_sample_rate / sample_rate)
|
||||||
|
data = scipy.signal.resample(data, num_samples)
|
||||||
|
sample_rate = self.target_sample_rate
|
||||||
|
# Cache only the clip region
|
||||||
|
start_sample = int(self.start * sample_rate)
|
||||||
|
end_sample = int(self.end * sample_rate) if self.end else len(data)
|
||||||
|
cached = data[start_sample:end_sample]
|
||||||
|
cached *= self.volume # Apply volume
|
||||||
|
return cached, sample_rate
|
||||||
|
|
||||||
|
def get_samples(self, num_samples):
|
||||||
|
# Return next chunk for playback
|
||||||
|
if self.position >= len(self.audio_data):
|
||||||
|
self.finished = True
|
||||||
|
return np.zeros(num_samples, dtype=np.float32)
|
||||||
|
end_pos = min(self.position + num_samples, len(self.audio_data))
|
||||||
|
chunk = self.audio_data[self.position:end_pos]
|
||||||
|
self.position = end_pos
|
||||||
|
if self.position >= len(self.audio_data):
|
||||||
|
self.finished = True
|
||||||
|
# Pad if chunk is short
|
||||||
|
if len(chunk) < num_samples:
|
||||||
|
chunk = np.pad(chunk, (0, num_samples - len(chunk)), mode='constant')
|
||||||
|
return chunk
|
||||||
|
|
||||||
|
def is_finished(self):
|
||||||
|
return self.finished
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.position = 0
|
||||||
|
self.finished = False
|
||||||
168
audio-service/src/audio_io.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import sounddevice as sd
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import scipy.io.wavfile as wavfile
|
||||||
|
from metadata_manager import MetaDataManager
|
||||||
|
from audio_clip import AudioClip
|
||||||
|
|
||||||
|
|
||||||
|
# AudioClip class for clip playback
|
||||||
|
|
||||||
|
|
||||||
|
class AudioIO:
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
# print("Creating new AudioRecorder instance")
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
|
return cls._instance
|
||||||
|
def init(self):
|
||||||
|
self.duration = 30
|
||||||
|
self.channels = 2
|
||||||
|
self.input_sample_rate = 44100
|
||||||
|
self.output_sample_rate = 44100
|
||||||
|
self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
|
||||||
|
self.recordings_dir = "recordings"
|
||||||
|
|
||||||
|
sd.default.latency = 'low'
|
||||||
|
|
||||||
|
self.socket = None
|
||||||
|
|
||||||
|
self.in_stream = sd.InputStream(
|
||||||
|
callback=self.record_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
self.out_stream = sd.OutputStream(
|
||||||
|
callback=self.playback_callback,
|
||||||
|
latency=3
|
||||||
|
)
|
||||||
|
|
||||||
|
self.clip_map = {}
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_streams(self):
|
||||||
|
was_active = self.in_stream.active
|
||||||
|
if was_active:
|
||||||
|
self.in_stream.stop()
|
||||||
|
self.out_stream.stop()
|
||||||
|
|
||||||
|
self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
|
||||||
|
# print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
|
||||||
|
self.in_stream = sd.InputStream(
|
||||||
|
callback=self.record_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
self.out_stream = sd.OutputStream(
|
||||||
|
callback=self.playback_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if was_active:
|
||||||
|
self.in_stream.start()
|
||||||
|
self.out_stream.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def record_callback(self, indata, frames, time, status):
|
||||||
|
if status:
|
||||||
|
# print(f"Recording status: {status}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Circular buffer implementation
|
||||||
|
self.buffer = np.roll(self.buffer, -frames, axis=0)
|
||||||
|
self.buffer[-frames:] = indata
|
||||||
|
|
||||||
|
def playback_callback(self, outdata, frames, time, status):
|
||||||
|
if status:
|
||||||
|
# print(f"Playback status: {status}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
outdata.fill(0)
|
||||||
|
|
||||||
|
# Iterate over a copy of the items to avoid modifying the dictionary during iteration
|
||||||
|
for clip_id, clip_list in list(self.clip_map.items()):
|
||||||
|
for clip in clip_list[:]: # Iterate over a copy of the list
|
||||||
|
if not clip.is_finished():
|
||||||
|
samples = clip.get_samples(frames)
|
||||||
|
outdata[:] += samples.reshape(-1, 1) # Mix into output
|
||||||
|
if clip.is_finished():
|
||||||
|
self.clip_map[clip_id].remove(clip)
|
||||||
|
if len(self.clip_map[clip_id]) == 0:
|
||||||
|
del self.clip_map[clip_id]
|
||||||
|
break # Exit inner loop since the key is deleted
|
||||||
|
|
||||||
|
|
||||||
|
def save_last_n_seconds(self):
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
os.makedirs(self.recordings_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Generate filename with timestamp
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav")
|
||||||
|
|
||||||
|
# Normalize audio to prevent clipping
|
||||||
|
audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
|
||||||
|
|
||||||
|
# Convert float32 to int16 for WAV file
|
||||||
|
audio_data_int16 = (audio_data * 32767).astype(np.int16)
|
||||||
|
|
||||||
|
# Write buffer to file
|
||||||
|
wavfile.write(filename, int(self.input_sample_rate), audio_data_int16)
|
||||||
|
|
||||||
|
meta = MetaDataManager()
|
||||||
|
|
||||||
|
clip_metadata = {
|
||||||
|
"filename": filename,
|
||||||
|
"name": f"Clip {timestamp}",
|
||||||
|
"playbackType":"playStop",
|
||||||
|
"volume": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.add_clip_to_collection("Uncategorized", clip_metadata )
|
||||||
|
self.socket.emit('new_clip', clip_metadata)
|
||||||
|
|
||||||
|
return clip_metadata
|
||||||
|
|
||||||
|
def set_buffer_duration(self, duration):
|
||||||
|
self.duration = duration
|
||||||
|
self.buffer = np.zeros((int(duration * self.input_sample_rate), self.channels), dtype=np.float32)
|
||||||
|
|
||||||
|
def set_recording_directory(self, directory):
|
||||||
|
self.recordings_dir = directory
|
||||||
|
|
||||||
|
def start_recording(self):
|
||||||
|
if(self.in_stream.active):
|
||||||
|
# print("Already recording")
|
||||||
|
return
|
||||||
|
# print('number of channels', self.channels)
|
||||||
|
|
||||||
|
self.in_stream.start()
|
||||||
|
self.out_stream.start()
|
||||||
|
self.output_sample_rate = self.out_stream.samplerate
|
||||||
|
self.input_sample_rate = self.in_stream.samplerate
|
||||||
|
|
||||||
|
def stop_recording(self):
|
||||||
|
if(not self.in_stream.active):
|
||||||
|
# print("Already stopped")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.in_stream.stop()
|
||||||
|
self.out_stream.stop()
|
||||||
|
|
||||||
|
def is_recording(self):
|
||||||
|
return self.in_stream.active
|
||||||
|
|
||||||
|
def play_clip(self, clip_metadata):
|
||||||
|
print(f"Playing clip: {clip_metadata}")
|
||||||
|
clip_id = clip_metadata.get("filename")
|
||||||
|
if clip_metadata.get("playbackType") == "playStop":
|
||||||
|
if clip_id in self.clip_map:
|
||||||
|
del self.clip_map[clip_id]
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.clip_map[clip_id] = []
|
||||||
|
if clip_id not in self.clip_map:
|
||||||
|
self.clip_map[clip_id] = []
|
||||||
|
self.clip_map[clip_id].append(AudioClip(clip_metadata, target_sample_rate=self.output_sample_rate))
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import sounddevice as sd
|
|
||||||
import numpy as np
|
|
||||||
import os
|
|
||||||
from datetime import datetime
|
|
||||||
import scipy.io.wavfile as wavfile
|
|
||||||
from metadata_manager import MetaDataManager
|
|
||||||
|
|
||||||
class AudioRecorder:
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
print("Creating new AudioRecorder instance")
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
cls._instance.init()
|
|
||||||
return cls._instance
|
|
||||||
def init(self):
|
|
||||||
"""
|
|
||||||
Initialize audio recorder with configurable parameters.
|
|
||||||
|
|
||||||
:param duration: Length of audio buffer in seconds
|
|
||||||
:param sample_rate: Audio sample rate (if None, use default device sample rate)
|
|
||||||
:param channels: Number of audio channels
|
|
||||||
"""
|
|
||||||
print(f"Initializing AudioRecorder")
|
|
||||||
self.duration = 30
|
|
||||||
self.sample_rate = 44100
|
|
||||||
self.channels = 2
|
|
||||||
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
|
|
||||||
self.recordings_dir = "recordings"
|
|
||||||
self.stream = sd.InputStream(
|
|
||||||
samplerate=self.sample_rate,
|
|
||||||
channels=self.channels,
|
|
||||||
callback=self.record_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
def refresh_stream(self):
|
|
||||||
"""
|
|
||||||
Refresh the audio stream with updated parameters.
|
|
||||||
"""
|
|
||||||
was_active = self.stream.active
|
|
||||||
if was_active:
|
|
||||||
self.stream.stop()
|
|
||||||
|
|
||||||
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
|
|
||||||
|
|
||||||
self.stream = sd.InputStream(
|
|
||||||
samplerate=self.sample_rate,
|
|
||||||
channels=self.channels,
|
|
||||||
callback=self.record_callback
|
|
||||||
)
|
|
||||||
|
|
||||||
if was_active:
|
|
||||||
self.stream.start()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def record_callback(self, indata, frames, time, status):
|
|
||||||
"""
|
|
||||||
Circular buffer callback for continuous recording.
|
|
||||||
|
|
||||||
:param indata: Input audio data
|
|
||||||
:param frames: Number of frames
|
|
||||||
:param time: Timestamp
|
|
||||||
:param status: Recording status
|
|
||||||
"""
|
|
||||||
if status:
|
|
||||||
print(f"Recording status: {status}")
|
|
||||||
|
|
||||||
# Circular buffer implementation
|
|
||||||
self.buffer = np.roll(self.buffer, -frames, axis=0)
|
|
||||||
self.buffer[-frames:] = indata
|
|
||||||
|
|
||||||
def save_last_n_seconds(self):
|
|
||||||
"""
|
|
||||||
Save the last n seconds of audio to a file.
|
|
||||||
|
|
||||||
:param output_dir: Directory to save recordings
|
|
||||||
:return: Path to saved audio file
|
|
||||||
"""
|
|
||||||
# Create output directory if it doesn't exist
|
|
||||||
os.makedirs(self.recordings_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Generate filename with timestamp
|
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
||||||
filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav")
|
|
||||||
|
|
||||||
# Normalize audio to prevent clipping
|
|
||||||
audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
|
|
||||||
|
|
||||||
# Convert float32 to int16 for WAV file
|
|
||||||
audio_data_int16 = (audio_data * 32767).astype(np.int16)
|
|
||||||
|
|
||||||
# Write buffer to file
|
|
||||||
wavfile.write(filename, int(self.sample_rate), audio_data_int16)
|
|
||||||
|
|
||||||
meta = MetaDataManager()
|
|
||||||
|
|
||||||
clip_metadata = {
|
|
||||||
"filename": filename,
|
|
||||||
"name": f"Clip {timestamp}",
|
|
||||||
"playbackType":"playStop",
|
|
||||||
"volume": 1.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
meta.add_clip_to_collection("Uncategorized", clip_metadata )
|
|
||||||
|
|
||||||
|
|
||||||
return clip_metadata
|
|
||||||
|
|
||||||
def set_buffer_duration(self, duration):
|
|
||||||
"""
|
|
||||||
Set the duration of the audio buffer.
|
|
||||||
|
|
||||||
:param duration: New buffer duration in seconds
|
|
||||||
"""
|
|
||||||
self.duration = duration
|
|
||||||
self.buffer = np.zeros((int(duration * self.sample_rate), self.channels), dtype=np.float32)
|
|
||||||
|
|
||||||
def set_recording_directory(self, directory):
|
|
||||||
"""
|
|
||||||
Set the directory where recordings will be saved.
|
|
||||||
|
|
||||||
:param directory: Path to the recordings directory
|
|
||||||
"""
|
|
||||||
self.recordings_dir = directory
|
|
||||||
|
|
||||||
def start_recording(self):
|
|
||||||
"""
|
|
||||||
Start continuous audio recording with circular buffer.
|
|
||||||
"""
|
|
||||||
if(self.stream.active):
|
|
||||||
print("Already recording")
|
|
||||||
return
|
|
||||||
print('number of channels', self.channels)
|
|
||||||
|
|
||||||
self.stream.start()
|
|
||||||
|
|
||||||
def stop_recording(self):
|
|
||||||
"""
|
|
||||||
Stop continuous audio recording with circular buffer.
|
|
||||||
"""
|
|
||||||
if(not self.stream.active):
|
|
||||||
print("Already stopped")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.stream.stop()
|
|
||||||
|
|
||||||
def is_recording(self):
|
|
||||||
"""
|
|
||||||
Check if the audio stream is currently active.
|
|
||||||
|
|
||||||
:return: True if recording, False otherwise
|
|
||||||
"""
|
|
||||||
return self.stream.active
|
|
||||||
@ -1,69 +1,94 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from audio_recorder import AudioRecorder
|
from audio_io import AudioIO
|
||||||
from windows_audio import WindowsAudioManager
|
from windows_audio import WindowsAudioManager
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
from metadata_manager import MetaDataManager
|
from metadata_manager import MetaDataManager
|
||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
|
|
||||||
from routes.recording import recording_bp
|
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 main():
|
def handle_connect():
|
||||||
# Create argument parser
|
print("Client connected")
|
||||||
parser = argparse.ArgumentParser(description='Audio Recording Service')
|
emit('full_data', MetaDataManager().collections)
|
||||||
|
|
||||||
# OSC port argument
|
@socketio.on('record_clip')
|
||||||
parser.add_argument('--osc-port',
|
def record_clip():
|
||||||
type=int,
|
io = AudioIO()
|
||||||
help='OSC server port number',
|
io.save_last_n_seconds();
|
||||||
default=5010)
|
|
||||||
|
@socketio.on('play_clip')
|
||||||
# Parse arguments
|
def play_clip(data):
|
||||||
args = parser.parse_args()
|
io = AudioIO()
|
||||||
audio_manager = WindowsAudioManager()
|
print(f"Received play_clip event with data: {data}")
|
||||||
settings = SettingsManager()
|
if data:
|
||||||
|
io.play_clip(data)
|
||||||
# Ensure save path exists
|
|
||||||
os.makedirs(settings.get_settings('save_path'), exist_ok=True)
|
|
||||||
|
def main():
|
||||||
|
# Create argument parser
|
||||||
# Register blueprints
|
parser = argparse.ArgumentParser(description='Audio Recording Service')
|
||||||
app.register_blueprint(recording_bp)
|
|
||||||
app.register_blueprint(device_bp)
|
# OSC port argument
|
||||||
app.register_blueprint(metadata_bp)
|
parser.add_argument('--osc-port',
|
||||||
app.register_blueprint(settings_bp)
|
type=int,
|
||||||
app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
help='OSC server port number',
|
||||||
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
default=5010)
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
args = parser.parse_args()
|
||||||
# Run the OSC server
|
audio_manager = WindowsAudioManager()
|
||||||
try:
|
settings = SettingsManager()
|
||||||
print(f"Starting OSC Recording Server on port {args.osc_port}")
|
meta = MetaDataManager()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# osc_server.run_server()
|
# Ensure save path exists
|
||||||
except KeyboardInterrupt:
|
os.makedirs(settings.get_settings('save_path'), exist_ok=True)
|
||||||
print("\nServer stopped by user.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error starting server: {e}")
|
io = AudioIO()
|
||||||
sys.exit(1)
|
io.start_recording()
|
||||||
|
|
||||||
|
# settings.socket = socketio
|
||||||
if __name__ == "__main__":
|
io.socket = socketio
|
||||||
|
meta.socket = socketio
|
||||||
|
|
||||||
|
# Register blueprints
|
||||||
|
app.register_blueprint(recording_bp)
|
||||||
|
app.register_blueprint(device_bp)
|
||||||
|
app.register_blueprint(metadata_bp)
|
||||||
|
app.register_blueprint(settings_bp)
|
||||||
|
print(f"Starting Flask server on port {settings.get_settings('http_port')}")
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Run the OSC server
|
||||||
|
# try:
|
||||||
|
# print(f"Starting OSC Recording Server on port {args.osc_port}")
|
||||||
|
# # osc_server.run_server()
|
||||||
|
# except KeyboardInterrupt:
|
||||||
|
# print("\nServer stopped by user.")
|
||||||
|
# except Exception as e:
|
||||||
|
# print(f"Error starting server: {e}")
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"Uncategorized": [
|
"Uncategorized": [
|
||||||
{
|
{
|
||||||
"endTime": 12.489270386266055,
|
"endTime": 12.489270386266055,
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav",
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav",
|
||||||
"name": "Clip 20260214_133540",
|
"name": "Clip 20260214_133540",
|
||||||
"playbackType": "playStop",
|
"playbackType": "playStop",
|
||||||
"startTime": 10.622317596566523,
|
"startTime": 10.622317596566523,
|
||||||
"volume": 1
|
"volume": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"endTime": 6.824034334763957,
|
"endTime": 6.824034334763957,
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav",
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav",
|
||||||
"name": "Clip 20260214_133137",
|
"name": "Clip 20260214_133137",
|
||||||
"playbackType": "playStop",
|
"playbackType": "playStop",
|
||||||
"startTime": 3.7982832618025753,
|
"startTime": 3.7982832618025753,
|
||||||
"volume": 1
|
"volume": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,100 +1,118 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from platformdirs import user_data_dir
|
||||||
class MetaDataManager:
|
|
||||||
_instance = None
|
class MetaDataManager:
|
||||||
|
_instance = None
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
def __new__(cls, *args, **kwargs):
|
||||||
cls._instance = super().__new__(cls)
|
if cls._instance is None:
|
||||||
cls._instance.init()
|
cls._instance = super().__new__(cls)
|
||||||
return cls._instance
|
cls._instance.init()
|
||||||
def init(self):
|
return cls._instance
|
||||||
# read metadata file from executing directory
|
def init(self):
|
||||||
self.metadata_file = os.path.join(os.getcwd(), "metadata.json")
|
self.socket = None
|
||||||
if os.path.exists(self.metadata_file):
|
# read metadata file from executing directory
|
||||||
with open(self.metadata_file, "r") as f:
|
file_path = user_data_dir("ClipTrim")
|
||||||
self.collections = json.load(f)
|
os.makedirs(file_path, exist_ok=True)
|
||||||
else:
|
print(file_path)
|
||||||
self.collections = {}
|
self.metadata_file = os.path.join(file_path, "metadata.json")
|
||||||
if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
|
if os.path.exists(self.metadata_file):
|
||||||
self.collections.append({"name": "Uncategorized", "id": 0, "clips": []})
|
with open(self.metadata_file, "r") as f:
|
||||||
self.save_metadata()
|
self.collections = json.load(f)
|
||||||
|
else:
|
||||||
def create_collection(self, name):
|
self.collections = []
|
||||||
if any(c.get("name") == name for c in self.collections):
|
if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
|
||||||
raise ValueError(f"Collection '{name}' already exists.")
|
self.collections.append({"name": "Uncategorized", "id": 0, "clips": []})
|
||||||
new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1
|
self.save_metadata()
|
||||||
self.collections.append({"name": name, "id": new_id, "clips": []})
|
|
||||||
self.save_metadata()
|
def create_collection(self, name):
|
||||||
|
if any(c.get("name") == name for c in self.collections):
|
||||||
def delete_collection(self, name):
|
raise ValueError(f"Collection '{name}' already exists.")
|
||||||
collection = next((c for c in self.collections if c.get("name") == name), None)
|
new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1
|
||||||
if collection is None:
|
self.collections.append({"name": name, "id": new_id, "clips": []})
|
||||||
raise ValueError(f"Collection '{name}' does not exist.")
|
self.save_metadata()
|
||||||
self.collections.remove(collection)
|
|
||||||
self.save_metadata()
|
def delete_collection(self, name):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == name), None)
|
||||||
def add_clip_to_collection(self, collection_name, clip_metadata):
|
if collection is None:
|
||||||
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
raise ValueError(f"Collection '{name}' does not exist.")
|
||||||
if collection is None:
|
self.collections.remove(collection)
|
||||||
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
self.save_metadata()
|
||||||
collection["clips"].append(clip_metadata)
|
|
||||||
self.save_metadata()
|
def add_clip_to_collection(self, collection_name, clip_metadata):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
def remove_clip_from_collection(self, collection_name, clip_metadata):
|
if collection is None:
|
||||||
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
if collection is None:
|
collection["clips"].append(clip_metadata)
|
||||||
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
if not self.socket is None:
|
||||||
# Remove all clips with the same file name as clip_metadata["file_name"]
|
self.socket.emit('collection_updated', collection)
|
||||||
in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"])
|
self.save_metadata()
|
||||||
if not in_list:
|
|
||||||
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
def remove_clip_from_collection(self, collection_name, clip_metadata):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
collection["clips"] = [
|
if collection is None:
|
||||||
clip for clip in collection["clips"]
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
if clip.get("filename") != clip_metadata.get("filename")
|
# Remove all clips with the same file name as clip_metadata["file_name"]
|
||||||
]
|
in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"])
|
||||||
self.save_metadata()
|
if not in_list:
|
||||||
|
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
||||||
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
|
|
||||||
self.remove_clip_from_collection(source_collection, clip_metadata)
|
collection["clips"] = [
|
||||||
self.add_clip_to_collection(target_collection, clip_metadata)
|
clip for clip in collection["clips"]
|
||||||
|
if clip.get("filename") != clip_metadata.get("filename")
|
||||||
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)
|
if not self.socket is None:
|
||||||
if collection is None:
|
self.socket.emit('collection_updated', collection)
|
||||||
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
self.save_metadata()
|
||||||
# Find the index of the clip with the same file name as old_clip_metadata["file_name"]
|
|
||||||
index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
|
|
||||||
if index is None:
|
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
|
||||||
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
self.remove_clip_from_collection(source_collection, clip_metadata)
|
||||||
|
self.add_clip_to_collection(target_collection, clip_metadata)
|
||||||
collection["clips"][index] = new_clip_metadata
|
if not self.socket is None:
|
||||||
self.save_metadata()
|
self.socket.emit('collection_updated', source_collection)
|
||||||
|
self.socket.emit('collection_updated', target_collection)
|
||||||
def get_collections(self):
|
|
||||||
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
|
|
||||||
|
def edit_clip_in_collection(self, collection_name, new_clip_metadata):
|
||||||
def get_clips_in_collection(self, collection_name):
|
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)
|
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.")
|
# Find the index of the clip with the same file name as old_clip_metadata["file_name"]
|
||||||
return collection["clips"]
|
index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
|
||||||
|
if index is None:
|
||||||
def reorder_clips_in_collection(self, collection_name, new_order):
|
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
||||||
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
|
||||||
if collection is None:
|
collection["clips"][index] = new_clip_metadata
|
||||||
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
if not self.socket is None:
|
||||||
existing_filenames = {clip.get("filename") for clip in collection["clips"]}
|
self.socket.emit('collection_updated', collection)
|
||||||
new_filenames = {clip.get("filename") for clip in new_order}
|
self.save_metadata()
|
||||||
|
|
||||||
if not new_filenames.issubset(existing_filenames):
|
def get_collections(self):
|
||||||
raise ValueError("New order contains clips that do not exist in the collection.")
|
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
|
||||||
|
|
||||||
collection["clips"] = new_order
|
def get_clips_in_collection(self, collection_name):
|
||||||
self.save_metadata()
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
def save_metadata(self):
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
with open(self.metadata_file, "w") as f:
|
return collection["clips"]
|
||||||
json.dump(self.collections, f, indent=4)
|
|
||||||
|
def reorder_clips_in_collection(self, collection_name, new_order):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
existing_filenames = {clip.get("filename") for clip in collection["clips"]}
|
||||||
|
new_filenames = {clip.get("filename") for clip in new_order}
|
||||||
|
|
||||||
|
if not new_filenames.issubset(existing_filenames):
|
||||||
|
raise ValueError("New order contains clips that do not exist in the collection.")
|
||||||
|
|
||||||
|
collection["clips"] = new_order
|
||||||
|
if not self.socket is None:
|
||||||
|
self.socket.emit('collection_updated', collection)
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def save_metadata(self):
|
||||||
|
with open(self.metadata_file, "w") as f:
|
||||||
|
json.dump(self.collections, f, indent=4)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
[ViewState]
|
[ViewState]
|
||||||
Mode=
|
Mode=
|
||||||
Vid=
|
Vid=
|
||||||
FolderType=Generic
|
FolderType=Generic
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from windows_audio import WindowsAudioManager
|
from windows_audio import WindowsAudioManager
|
||||||
from audio_recorder import AudioRecorder
|
from audio_io import AudioIO
|
||||||
|
|
||||||
device_bp = Blueprint('device', __name__)
|
device_bp = Blueprint('device', __name__)
|
||||||
|
|
||||||
audio_manager = WindowsAudioManager()
|
audio_manager = WindowsAudioManager()
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
|
|
||||||
# @device_bp.route('/device/set', methods=['POST'])
|
# @device_bp.route('/device/set', methods=['POST'])
|
||||||
# def set_audio_device():
|
# def set_audio_device():
|
||||||
@ -19,13 +19,13 @@ recorder = AudioRecorder()
|
|||||||
# except Exception as e:
|
# except Exception as e:
|
||||||
# return jsonify({'status': 'error', 'message': str(e)}), 400
|
# return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@device_bp.route('/device/get', methods=['GET'])
|
# @device_bp.route('/device/get', methods=['GET'])
|
||||||
def get_audio_device():
|
# def get_audio_device():
|
||||||
try:
|
# try:
|
||||||
device_info = audio_manager.get_default_device('input')
|
# device_info = audio_manager.get_default_device('input')
|
||||||
return jsonify({'status': 'success', 'device_info': device_info})
|
# return jsonify({'status': 'success', 'device_info': device_info})
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
# return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@device_bp.route('/device/list', methods=['GET'])
|
@device_bp.route('/device/list', methods=['GET'])
|
||||||
def list_audio_devices():
|
def list_audio_devices():
|
||||||
|
|||||||
@ -91,10 +91,16 @@ def edit_clip_in_collection():
|
|||||||
meta_manager = MetaDataManager()
|
meta_manager = MetaDataManager()
|
||||||
collection_name = request.json.get('name')
|
collection_name = request.json.get('name')
|
||||||
clip_metadata = request.json.get('clip')
|
clip_metadata = request.json.get('clip')
|
||||||
print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}")
|
# print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}")
|
||||||
try:
|
try:
|
||||||
meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
|
meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
|
||||||
collections = meta_manager.collections
|
collections = meta_manager.collections
|
||||||
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'})
|
||||||
@ -1,53 +1,57 @@
|
|||||||
|
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from audio_recorder import AudioRecorder
|
from audio_io import AudioIO
|
||||||
import os
|
import os
|
||||||
|
|
||||||
recording_bp = Blueprint('recording', __name__)
|
recording_bp = Blueprint('recording', __name__)
|
||||||
|
|
||||||
@recording_bp.route('/record/start', methods=['POST'])
|
@recording_bp.route('/record/start', methods=['POST'])
|
||||||
def start_recording():
|
def start_recording():
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
print('HTTP: Starting audio recording')
|
print('HTTP: Starting audio recording')
|
||||||
recorder.start_recording()
|
recorder.start_recording()
|
||||||
return jsonify({'status': 'recording started'})
|
return jsonify({'status': 'recording started'})
|
||||||
|
|
||||||
@recording_bp.route('/record/stop', methods=['POST'])
|
@recording_bp.route('/record/stop', methods=['POST'])
|
||||||
def stop_recording():
|
def stop_recording():
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
print('HTTP: Stopping audio recording')
|
# print('HTTP: Stopping audio recording')
|
||||||
recorder.stop_recording()
|
recorder.stop_recording()
|
||||||
return jsonify({'status': 'recording stopped'})
|
return jsonify({'status': 'recording stopped'})
|
||||||
|
|
||||||
@recording_bp.route('/record/save', methods=['POST'])
|
@recording_bp.route('/record/save', methods=['POST'])
|
||||||
def save_recording():
|
def save_recording():
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
print('HTTP: Saving audio recording')
|
# print('HTTP: Saving audio recording')
|
||||||
saved_file = recorder.save_last_n_seconds()
|
saved_file = recorder.save_last_n_seconds()
|
||||||
return jsonify({'status': 'recording saved', 'file': saved_file})
|
return jsonify({'status': 'recording saved', 'file': saved_file})
|
||||||
|
|
||||||
|
|
||||||
@recording_bp.route('/record/status', methods=['GET'])
|
@recording_bp.route('/record/status', methods=['GET'])
|
||||||
def recording_status():
|
def recording_status():
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
print('HTTP: Checking recording status')
|
# print('HTTP: Checking recording status')
|
||||||
status = 'recording' if recorder.is_recording() else 'stopped'
|
status = 'recording' if recorder.is_recording() else 'stopped'
|
||||||
return jsonify({'status': status})
|
return jsonify({'status': status})
|
||||||
|
|
||||||
@recording_bp.route('/record/delete', methods=['POST'])
|
@recording_bp.route('/record/delete', methods=['POST'])
|
||||||
def recording_delete():
|
def recording_delete():
|
||||||
filename = request.json.get('filename')
|
filename = request.json.get('filename')
|
||||||
try:
|
try:
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
return jsonify({'status': 'success'})
|
return jsonify({'status': 'success'})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@recording_bp.route('/playback/start', methods=['POST'])
|
@recording_bp.route('/playback/start', methods=['POST'])
|
||||||
def playback_start():
|
def playback_start():
|
||||||
print('HTTP: Starting audio playback')
|
print(f"Playing clip")
|
||||||
try:
|
# print('HTTP: Starting audio playback')
|
||||||
# os.remove(filename)
|
clip = request.json
|
||||||
return jsonify({'status': 'success'})
|
try:
|
||||||
except Exception as e:
|
io = AudioIO()
|
||||||
|
io.play_clip(clip)
|
||||||
|
# os.remove(filename)
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
@ -1,25 +1,32 @@
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
|
|
||||||
settings_bp = Blueprint('settings', __name__)
|
settings_bp = Blueprint('settings', __name__)
|
||||||
|
|
||||||
|
|
||||||
@settings_bp.route('/settings', methods=['GET'])
|
@settings_bp.route('/settings', methods=['GET'])
|
||||||
def get_all_settings():
|
def get_all_settings():
|
||||||
return jsonify({'status': 'success', 'settings': SettingsManager().get_all_settings()})
|
return jsonify({'status': 'success', 'settings': SettingsManager().get_all_settings()})
|
||||||
|
|
||||||
@settings_bp.route('/settings/<name>', methods=['GET'])
|
@settings_bp.route('/settings/<name>', methods=['GET'])
|
||||||
def get_setting(name):
|
def get_setting(name):
|
||||||
value = SettingsManager().get_settings(name)
|
value = SettingsManager().get_settings(name)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return jsonify({'status': 'success', 'name': name, 'value': value})
|
return jsonify({'status': 'success', 'name': name, 'value': value})
|
||||||
else:
|
else:
|
||||||
return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404
|
return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404
|
||||||
|
|
||||||
@settings_bp.route('/settings/<name>', methods=['POST'])
|
@settings_bp.route('/settings/update', methods=['POST'])
|
||||||
def set_setting(name):
|
def set_all_settings():
|
||||||
value = request.json.get('value')
|
settings = request.json.get('settings')
|
||||||
if value is None:
|
print (f"Received settings update: {settings}")
|
||||||
return jsonify({'status': 'error', 'message': 'Value is required'}), 400
|
if settings is None:
|
||||||
SettingsManager().set_settings(name, value)
|
return jsonify({'status': 'error', 'message': 'Settings are required'}), 400
|
||||||
return jsonify({'status': 'success', 'name': name, 'value': value})
|
try:
|
||||||
|
for name, value in settings.items():
|
||||||
|
print(f"Updating setting '{name}' to '{value}'")
|
||||||
|
SettingsManager().set_settings(name, value)
|
||||||
|
return jsonify({'status': 'success', 'settings': settings})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"input_device": {
|
"input_device": {
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"name": "Microsoft Sound Mapper - Input",
|
"name": "Microsoft Sound Mapper - Input",
|
||||||
"max_input_channels": 2,
|
"max_input_channels": 2,
|
||||||
"default_samplerate": 44100.0
|
"default_samplerate": 44100.0
|
||||||
},
|
},
|
||||||
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings",
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings",
|
||||||
"recording_length": 15
|
"recording_length": 15
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from audio_recorder import AudioRecorder
|
|
||||||
|
from platformdirs import user_data_dir
|
||||||
|
from audio_io import AudioIO
|
||||||
from windows_audio import WindowsAudioManager
|
from windows_audio import WindowsAudioManager
|
||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
@ -13,28 +15,59 @@ class SettingsManager:
|
|||||||
return cls._instance
|
return cls._instance
|
||||||
def init(self):
|
def init(self):
|
||||||
# read settings file from executing directory
|
# read settings file from executing directory
|
||||||
self.settings_file = os.path.join(os.getcwd(), "settings.json")
|
print("Initializing SettingsManager", os.getcwd())
|
||||||
|
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)
|
||||||
else:
|
else:
|
||||||
self.settings = {
|
self.settings = {
|
||||||
"input_device": None,
|
"input_device": None,
|
||||||
"save_path": os.path.join(os.getcwd(), "recordings"),
|
"output_device": None,
|
||||||
|
"save_path": os.path.join(file_path, "recordings"),
|
||||||
"recording_length": 15
|
"recording_length": 15
|
||||||
}
|
}
|
||||||
audio_manager = WindowsAudioManager()
|
audio_manager = WindowsAudioManager()
|
||||||
|
|
||||||
devices = audio_manager.list_audio_devices('input')
|
input_devices = audio_manager.list_audio_devices('input')
|
||||||
print(f"Available input devices: {self.settings}")
|
output_devices = audio_manager.list_audio_devices('output')
|
||||||
input = self.settings["input_device"]
|
# print("Available input devices:")
|
||||||
|
# for i, dev in enumerate(input_devices):
|
||||||
|
# print(i, dev['name'])
|
||||||
|
# print("Available output devices:")
|
||||||
|
# for i, dev in enumerate(output_devices):
|
||||||
|
# print(i, dev['name'])
|
||||||
|
# print(f"Available input devices: {input_devices}")
|
||||||
|
# print(f"Available output devices: {output_devices}")
|
||||||
|
input = None
|
||||||
|
output = None
|
||||||
|
|
||||||
|
if("input_device" in self.settings):
|
||||||
|
input = self.settings["input_device"]
|
||||||
|
if("output_device" in self.settings):
|
||||||
|
output = self.settings["output_device"]
|
||||||
#see if input device is in "devices", if not set to the first index
|
#see if input device is in "devices", if not set to the first index
|
||||||
if input is not None and any(d['name'] == input["name"] for d in devices):
|
if input is not None and any(d['name'] == input["name"] for d in input_devices):
|
||||||
print(f"Using saved input device index: {input}")
|
# print(f"Using saved input device index: {input}")
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
input = devices[0] if devices else None
|
input = input_devices[0] if input_devices else None
|
||||||
self.settings["input_device"] = input
|
self.settings["input_device"] = input
|
||||||
|
|
||||||
|
#see if output device is in "devices", if not set to the first index
|
||||||
|
if output is not None and any(d['name'] == output["name"] for d in output_devices):
|
||||||
|
# print(f"Using saved output device index: {output}")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
output = output_devices[0] if output_devices else None
|
||||||
|
self.settings["output_device"] = output
|
||||||
|
|
||||||
|
if not "http_port" in self.settings:
|
||||||
|
self.settings["http_port"] = 5010
|
||||||
|
|
||||||
|
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
|
|
||||||
@ -48,6 +81,8 @@ class SettingsManager:
|
|||||||
return self.settings
|
return self.settings
|
||||||
|
|
||||||
def set_settings(self, name, value):
|
def set_settings(self, name, value):
|
||||||
|
if(name not in self.settings):
|
||||||
|
raise ValueError(f"Setting '{name}' not found.")
|
||||||
self.settings[name] = value
|
self.settings[name] = value
|
||||||
self.save_settings()
|
self.save_settings()
|
||||||
|
|
||||||
@ -57,13 +92,14 @@ class SettingsManager:
|
|||||||
json.dump(self.settings, f, indent=4)
|
json.dump(self.settings, f, indent=4)
|
||||||
|
|
||||||
def refresh_settings(self):
|
def refresh_settings(self):
|
||||||
recorder = AudioRecorder()
|
recorder = AudioIO()
|
||||||
# Update recorder parameters based on new setting
|
# Update recorder parameters based on new setting
|
||||||
recorder.set_buffer_duration(self.get_settings('recording_length'))
|
recorder.set_buffer_duration(self.get_settings('recording_length'))
|
||||||
recorder.recordings_dir = self.get_settings('save_path')
|
recorder.recordings_dir = self.get_settings('save_path')
|
||||||
|
|
||||||
audio_manager = WindowsAudioManager()
|
audio_manager = WindowsAudioManager()
|
||||||
audio_manager.set_default_input_device(self.get_settings('input_device')['index'])
|
audio_manager.set_default_input_device(self.get_settings('input_device')['index'])
|
||||||
|
audio_manager.set_default_output_device(self.get_settings('output_device')['index'])
|
||||||
|
|
||||||
recorder.refresh_stream()
|
recorder.refresh_streams()
|
||||||
|
|
||||||
|
|||||||
@ -1,123 +1,108 @@
|
|||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import comtypes
|
import json
|
||||||
import comtypes.client
|
|
||||||
from comtypes import CLSCTX_ALL
|
class WindowsAudioManager:
|
||||||
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
_instance = None
|
||||||
import json
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
class WindowsAudioManager:
|
if cls._instance is None:
|
||||||
_instance = None
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
def __new__(cls, *args, **kwargs):
|
return cls._instance
|
||||||
if cls._instance is None:
|
def init(self):
|
||||||
cls._instance = super().__new__(cls)
|
"""
|
||||||
cls._instance.init()
|
Initialize Windows audio device and volume management.
|
||||||
return cls._instance
|
"""
|
||||||
def init(self):
|
host_apis = sd.query_hostapis()
|
||||||
"""
|
wasapi_device_indexes = None
|
||||||
Initialize Windows audio device and volume management.
|
for api in host_apis:
|
||||||
"""
|
if api['name'].lower() == 'Windows WASAPI'.lower():
|
||||||
self.devices = sd.query_devices()
|
wasapi_device_indexes = api['devices']
|
||||||
self.default_input = sd.default.device[0]
|
break
|
||||||
self.default_output = sd.default.device[1]
|
# print(f"Host APIs: {host_apis}")
|
||||||
|
# print(f"WASAPI Device Indexes: {wasapi_device_indexes}")
|
||||||
def list_audio_devices(self, kind='input'):
|
wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set()
|
||||||
"""
|
self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes]
|
||||||
List available audio devices.
|
# self.devices = sd.query_devices()
|
||||||
|
# print(f"devices: {self.devices}")
|
||||||
:param kind: 'input' or 'output'
|
|
||||||
:return: List of audio devices
|
self.default_input = sd.default.device[0]
|
||||||
"""
|
self.default_output = sd.default.device[1]
|
||||||
if kind == 'input':
|
|
||||||
return [
|
def list_audio_devices(self, kind='input'):
|
||||||
{
|
"""
|
||||||
'index': dev['index'],
|
List available audio devices.
|
||||||
'name': dev['name'],
|
|
||||||
'max_input_channels': dev['max_input_channels'],
|
:param kind: 'input' or 'output'
|
||||||
'default_samplerate': dev['default_samplerate']
|
:return: List of audio devices
|
||||||
}
|
"""
|
||||||
for dev in self.devices if dev['max_input_channels'] > 0
|
if kind == 'input':
|
||||||
]
|
return [
|
||||||
elif kind == 'output':
|
{
|
||||||
return [
|
'index': dev['index'],
|
||||||
{
|
'name': dev['name'],
|
||||||
'index': dev['index'],
|
'channels': dev['max_input_channels'],
|
||||||
'name': dev['name'],
|
'default_samplerate': dev['default_samplerate']
|
||||||
'max_output_channels': dev['max_output_channels'],
|
}
|
||||||
'default_samplerate': dev['default_samplerate']
|
for dev in self.devices if dev['max_input_channels'] > 0
|
||||||
}
|
]
|
||||||
for dev in self.devices if dev['max_output_channels'] > 0
|
elif kind == 'output':
|
||||||
]
|
return [
|
||||||
def get_default_device(self, kind='input'):
|
{
|
||||||
"""
|
'index': dev['index'],
|
||||||
Get the default audio device.
|
'name': dev['name'],
|
||||||
|
'channels': dev['max_output_channels'],
|
||||||
:param kind: 'input' or 'output'
|
'default_samplerate': dev['default_samplerate']
|
||||||
:return: Default audio device information
|
}
|
||||||
"""
|
for dev in self.devices if dev['max_output_channels'] > 0
|
||||||
if kind == 'input':
|
]
|
||||||
dev = self.devices[self.default_input]
|
def get_default_device(self, kind='input'):
|
||||||
return [
|
"""
|
||||||
{
|
Get the default audio device.
|
||||||
'index': dev['index'],
|
|
||||||
'name': dev['name'],
|
:param kind: 'input' or 'output'
|
||||||
'max_input_channels': dev['max_input_channels'],
|
:return: Default audio device information
|
||||||
'default_samplerate': dev['default_samplerate']
|
"""
|
||||||
}
|
if kind == 'input':
|
||||||
]
|
dev = self.devices[self.default_input]
|
||||||
|
return [
|
||||||
def set_default_input_device(self, device_index):
|
{
|
||||||
if(device_index is None):
|
'index': dev['index'],
|
||||||
return self.get_current_input_device_sample_rate()
|
'name': dev['name'],
|
||||||
"""
|
'max_input_channels': dev['max_input_channels'],
|
||||||
Set the default input audio device.
|
'default_samplerate': dev['default_samplerate']
|
||||||
|
}
|
||||||
:param device_index: Index of the audio device
|
]
|
||||||
:return: Sample rate of the selected device
|
|
||||||
"""
|
def set_default_input_device(self, device_index):
|
||||||
sd.default.device[0] = device_index
|
if(device_index is None):
|
||||||
self.default_input = device_index
|
return 0
|
||||||
|
"""
|
||||||
# Get the sample rate of the selected device
|
Set the default input audio device.
|
||||||
device_info = sd.query_devices(device_index)
|
|
||||||
return device_info['default_samplerate']
|
:param device_index: Index of the audio device
|
||||||
|
:return: Sample rate of the selected device
|
||||||
def get_current_input_device_sample_rate(self):
|
"""
|
||||||
"""
|
sd.default.device[0] = device_index
|
||||||
Get the sample rate of the current input device.
|
self.default_input = device_index
|
||||||
|
|
||||||
:return: Sample rate of the current input device
|
# Get the sample rate of the selected device
|
||||||
"""
|
device_info = sd.query_devices(device_index)
|
||||||
device_info = sd.query_devices(self.default_input)
|
return device_info['default_samplerate']
|
||||||
return device_info['default_samplerate']
|
|
||||||
|
def set_default_output_device(self, device_index):
|
||||||
def get_system_volume(self):
|
if(device_index is None):
|
||||||
"""
|
return self.get_current_output_device_sample_rate()
|
||||||
Get the system master volume.
|
"""
|
||||||
|
Set the default output audio device.
|
||||||
:return: Current system volume (0.0 to 1.0)
|
|
||||||
"""
|
:param device_index: Index of the audio device
|
||||||
devices = AudioUtilities.GetSpeakers()
|
:return: Sample rate of the selected device
|
||||||
interface = devices.Activate(
|
"""
|
||||||
IAudioEndpointVolume._iid_,
|
sd.default.device[1] = device_index
|
||||||
CLSCTX_ALL,
|
self.default_output = device_index
|
||||||
None
|
|
||||||
)
|
# Get the sample rate of the selected device
|
||||||
volume = interface.QueryInterface(IAudioEndpointVolume)
|
device_info = sd.query_devices(device_index)
|
||||||
return volume.GetMasterVolumeLevelScalar()
|
return device_info['default_samplerate']
|
||||||
|
|
||||||
def set_system_volume(self, volume_level):
|
|
||||||
"""
|
|
||||||
Set the system master volume.
|
|
||||||
|
|
||||||
:param volume_level: Volume level (0.0 to 1.0)
|
|
||||||
"""
|
|
||||||
devices = AudioUtilities.GetSpeakers()
|
|
||||||
interface = devices.Activate(
|
|
||||||
IAudioEndpointVolume._iid_,
|
|
||||||
CLSCTX_ALL,
|
|
||||||
None
|
|
||||||
)
|
|
||||||
volume = interface.QueryInterface(IAudioEndpointVolume)
|
|
||||||
volume.SetMasterVolumeLevelScalar(volume_level, None)
|
|
||||||
@ -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 = {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
electron-ui/assets/tray_icon.png
Normal file
|
After Width: | Height: | Size: 780 B |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
electron-ui/dll_err.txt
Normal file
614
electron-ui/package-lock.json
generated
@ -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": {
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
8
electron-ui/release/app/package-lock.json
generated
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
16
electron-ui/settings.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"input_device": {
|
||||||
|
"index": 49,
|
||||||
|
"name": "Microphone (Logi C615 HD WebCam)",
|
||||||
|
"channels": 1,
|
||||||
|
"default_samplerate": 48000.0
|
||||||
|
},
|
||||||
|
"output_device": {
|
||||||
|
"index": 40,
|
||||||
|
"name": "Speakers (Realtek(R) Audio)",
|
||||||
|
"channels": 2,
|
||||||
|
"default_samplerate": 48000.0
|
||||||
|
},
|
||||||
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\electron-ui\\recordings",
|
||||||
|
"recording_length": 15
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
electron-ui/src/assets/tray_icon.png
Normal file
|
After Width: | Height: | Size: 780 B |
@ -1,5 +1,7 @@
|
|||||||
const AudioChannels = {
|
const AudioChannels = {
|
||||||
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
||||||
|
GET_PORT: 'audio:getPort',
|
||||||
|
RESTART_SERVICE: 'audio:restartService',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default AudioChannels;
|
export default AudioChannels;
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { 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';
|
||||||
|
|
||||||
export default function registerAudioIpcHandlers() {
|
export default function registerAudioIpcHandlers() {
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
@ -15,4 +16,25 @@ export default function registerAudioIpcHandlers() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(AudioChannels.GET_PORT, async () => {
|
||||||
|
try {
|
||||||
|
if (PythonSubprocessManager.instance?.portNumber) {
|
||||||
|
return { port: PythonSubprocessManager.instance.portNumber };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: 'Port number not available yet.' };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(AudioChannels.RESTART_SERVICE, async () => {
|
||||||
|
try {
|
||||||
|
PythonSubprocessManager.instance?.restart();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { success: false, error: err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,3 +6,22 @@ export interface LoadAudioBufferResult {
|
|||||||
buffer?: Buffer;
|
buffer?: Buffer;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetPortResult {
|
||||||
|
port?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetPortArgs {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetPortResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestartServiceResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -10,12 +10,15 @@
|
|||||||
*/
|
*/
|
||||||
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 } 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/audio/main';
|
||||||
|
import PythonSubprocessManager from './service';
|
||||||
|
|
||||||
|
const pythonManager = new PythonSubprocessManager('src/main.py');
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -25,7 +28,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}`;
|
||||||
@ -75,7 +79,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')
|
||||||
@ -96,12 +100,15 @@ 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();
|
||||||
|
|
||||||
// Open urls in the user's browser
|
// Open urls in the user's browser
|
||||||
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
mainWindow.webContents.setWindowOpenHandler((edata) => {
|
||||||
@ -109,10 +116,34 @@ const createWindow = async () => {
|
|||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
registerFileIpcHandlers();
|
|
||||||
// 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();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,14 +153,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
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
// 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 FileChannels from '../ipc/audio/channels';
|
import { LoadAudioBufferArgs } from '../ipc/audio/types';
|
||||||
import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types';
|
|
||||||
import AudioChannels from '../ipc/audio/channels';
|
import AudioChannels from '../ipc/audio/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
|
||||||
|
|
||||||
@ -41,10 +40,8 @@ const audioHandler = {
|
|||||||
filePath,
|
filePath,
|
||||||
} satisfies LoadAudioBufferArgs),
|
} satisfies LoadAudioBufferArgs),
|
||||||
|
|
||||||
readText: (filePath: string) =>
|
getPort: () => ipcRenderer.invoke(AudioChannels.GET_PORT),
|
||||||
ipcRenderer.invoke(AudioChannels.READ_TEXT, {
|
restartService: () => ipcRenderer.invoke(AudioChannels.RESTART_SERVICE),
|
||||||
filePath,
|
|
||||||
} satisfies ReadTextArgs),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('audio', audioHandler);
|
contextBridge.exposeInMainWorld('audio', audioHandler);
|
||||||
|
|||||||
81
electron-ui/src/main/service.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default class PythonSubprocessManager {
|
||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
public static instance: PythonSubprocessManager | null = null;
|
||||||
|
|
||||||
|
private process: ChildProcessWithoutNullStreams | null = null;
|
||||||
|
|
||||||
|
private scriptPath: string;
|
||||||
|
|
||||||
|
private working_dir: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'audio-service',
|
||||||
|
);
|
||||||
|
|
||||||
|
public portNumber: number | null = null;
|
||||||
|
|
||||||
|
constructor(scriptPath: string) {
|
||||||
|
this.scriptPath = scriptPath;
|
||||||
|
PythonSubprocessManager.instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(args: string[] = []): void {
|
||||||
|
if (this.process) {
|
||||||
|
throw new Error('Process already running.');
|
||||||
|
}
|
||||||
|
console.log(`Using Python working directory at: ${this.working_dir}`);
|
||||||
|
console.log(`Starting Python subprocess with script: ${this.scriptPath}`);
|
||||||
|
this.process = spawn(
|
||||||
|
'venv/Scripts/python.exe',
|
||||||
|
[this.scriptPath, ...args],
|
||||||
|
{
|
||||||
|
cwd: this.working_dir,
|
||||||
|
detached: false,
|
||||||
|
stdio: 'pipe',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this.process.stdout.on('data', (data: Buffer) => {
|
||||||
|
// console.log(`Python stdout: ${data.toString()}`);
|
||||||
|
});
|
||||||
|
this.process.stderr.on('data', (data: Buffer) => {
|
||||||
|
// console.error(`Python stderr: ${data.toString()}`);
|
||||||
|
const lines = data.toString().split('\n');
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const line of lines) {
|
||||||
|
const match = line.match(/Running on .*:(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
const port = parseInt(match[1], 10);
|
||||||
|
console.log(`Detected port: ${port}`);
|
||||||
|
this.portNumber = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.process.on('exit', () => {
|
||||||
|
console.log('Python subprocess exited.');
|
||||||
|
this.process = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(): void {
|
||||||
|
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 = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restart(args: string[] = []): void {
|
||||||
|
this.stop();
|
||||||
|
this.start(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
isHealthy(): boolean {
|
||||||
|
return !!this.process && !this.process.killed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -2,14 +2,20 @@ import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
|
||||||
|
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';
|
||||||
import { useAppDispatch, useAppSelector } from './hooks';
|
import { useAppDispatch, useAppSelector } from './hooks';
|
||||||
import { store } from '../redux/main';
|
import { store } from '../redux/main';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import SettingsPage from './Settings';
|
||||||
|
import { apiFetch, getBaseUrl } from './api';
|
||||||
|
|
||||||
function MainPage() {
|
function MainPage() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -21,20 +27,42 @@ function MainPage() {
|
|||||||
);
|
);
|
||||||
const [newCollectionOpen, setNewCollectionOpen] = useState(false);
|
const [newCollectionOpen, setNewCollectionOpen] = useState(false);
|
||||||
const [newCollectionName, setNewCollectionName] = useState<string>('');
|
const [newCollectionName, setNewCollectionName] = useState<string>('');
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMetadata = async () => {
|
let newSocket: any = null;
|
||||||
try {
|
const initializeSocket = async () => {
|
||||||
const response = await fetch('http://localhost:5010/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(() => {
|
||||||
@ -137,6 +165,22 @@ function MainPage() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
{/* Settings Button at Bottom Left */}
|
||||||
|
<div className="mt-auto mb-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full rounded px-4 py-2 bg-neutral-800 text-offwhite hover:bg-plumDark text-left"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 16,
|
||||||
|
left: 8,
|
||||||
|
width: 'calc(100% - 16px)',
|
||||||
|
}}
|
||||||
|
onClick={() => navigate('/settings')}
|
||||||
|
>
|
||||||
|
⚙️ Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div
|
<div
|
||||||
@ -150,13 +194,39 @@ function MainPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const theme = createTheme({
|
||||||
|
colorSchemes: {
|
||||||
|
light: false,
|
||||||
|
dark: {
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#6e44ba', // plum
|
||||||
|
dark: '#6e44ba', // plum
|
||||||
|
contrastText: '#ffffff',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#4f3186', // plumDark
|
||||||
|
dark: '#4f3186', // plumDark
|
||||||
|
contrastText: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// colorSchemes: {
|
||||||
|
// light: false,
|
||||||
|
// dark: true,
|
||||||
|
// },
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router>
|
<ThemeProvider theme={theme}>
|
||||||
<Routes>
|
<Router>
|
||||||
<Route path="/" element={<MainPage />} />
|
<Routes>
|
||||||
</Routes>
|
<Route path="/" element={<MainPage />} />
|
||||||
</Router>
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</ThemeProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
274
electron-ui/src/renderer/Settings.tsx
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import './App.css';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import { apiFetch } from './api';
|
||||||
|
|
||||||
|
type AudioDevice = {
|
||||||
|
index: number;
|
||||||
|
name: string;
|
||||||
|
default_sample_rate: number;
|
||||||
|
channels: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Settings = {
|
||||||
|
http_port: number;
|
||||||
|
input_device: AudioDevice;
|
||||||
|
output_device: AudioDevice;
|
||||||
|
recording_length: number;
|
||||||
|
save_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
http_port: 0,
|
||||||
|
input_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
|
||||||
|
output_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
|
||||||
|
recording_length: 0,
|
||||||
|
save_path: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchAudioDevices(
|
||||||
|
type: 'input' | 'output',
|
||||||
|
): Promise<AudioDevice[]> {
|
||||||
|
// Replace with actual backend call
|
||||||
|
// Example: return window.api.getAudioDevices();
|
||||||
|
return apiFetch(`device/list?device_type=${type}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.devices as AudioDevice[])
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error fetching audio devices:', error);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSettings(): Promise<Settings> {
|
||||||
|
// Replace with actual backend call
|
||||||
|
// Example: return window.api.getAudioDevices();
|
||||||
|
console.log('Fetching settings from backend...');
|
||||||
|
return apiFetch('settings')
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => data.settings as Settings)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error fetching settings:', error);
|
||||||
|
return defaultSettings;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSettingsToBackend = async (settings: Settings) => {
|
||||||
|
// Replace with actual backend call
|
||||||
|
// Example: window.api.updateSettings(settings);
|
||||||
|
console.log('Settings updated:', settings);
|
||||||
|
await apiFetch('settings/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ settings }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.log('Settings update response:', data);
|
||||||
|
if (data.status === 'success') {
|
||||||
|
window.audio.restartService();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error updating settings:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const [settings, setSettings] = useState<Settings>(defaultSettings);
|
||||||
|
const [inputDevices, setInputDevices] = useState<AudioDevice[]>([]);
|
||||||
|
const [outputDevices, setOutputDevices] = useState<AudioDevice[]>([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSettings()
|
||||||
|
.then((fetchedSettings) => {
|
||||||
|
console.log('Fetched settings:', fetchedSettings);
|
||||||
|
setSettings(fetchedSettings);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return fetchAudioDevices('input');
|
||||||
|
})
|
||||||
|
.then((devices) => {
|
||||||
|
setInputDevices(devices);
|
||||||
|
// console.log('Input devices:', devices);
|
||||||
|
return fetchAudioDevices('output');
|
||||||
|
})
|
||||||
|
.then((devices) => {
|
||||||
|
setOutputDevices(devices);
|
||||||
|
|
||||||
|
// console.log('Output devices:', devices);
|
||||||
|
return devices;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error fetching audio devices:', error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {}, [settings]);
|
||||||
|
|
||||||
|
const handleChange = () => {
|
||||||
|
sendSettingsToBackend(settings);
|
||||||
|
// const { name, value } = e.target;
|
||||||
|
// setSettings((prev) => ({
|
||||||
|
// ...prev,
|
||||||
|
// [name]: value,
|
||||||
|
// }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFolderChange = async () => {
|
||||||
|
// Replace with actual folder picker
|
||||||
|
// Example: const folder = await window.api.selectFolder();
|
||||||
|
// const folder = window.prompt(
|
||||||
|
// 'Enter output folder path:',
|
||||||
|
// settings.outputFolder,
|
||||||
|
// );
|
||||||
|
// if (folder !== null) {
|
||||||
|
// setSettings((prev) => ({
|
||||||
|
// ...prev,
|
||||||
|
// outputFolder: folder,
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-screen min-h-screen bg-midnight text-offwhite flex items-center justify-center relative">
|
||||||
|
<div className="w-3/4 min-w-[600px] max-w-[800px] self-start flex flex-col font-sans bg-midnight text-offwhite p-6 rounded-lg relative">
|
||||||
|
{/* X Close Button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute top-6 right-6 text-3xl font-bold text-offwhite bg-transparent hover:text-plumDark"
|
||||||
|
aria-label="Close settings"
|
||||||
|
onClick={() => navigate('/')}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<span className="text-2xl font-bold mb-4">Settings</span>
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<span>HTTP Port:</span>
|
||||||
|
<TextField
|
||||||
|
variant="standard"
|
||||||
|
type="text"
|
||||||
|
name="httpPort"
|
||||||
|
value={settings.http_port}
|
||||||
|
onBlur={() => handleChange()}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!Number.isNaN(Number(e.target.value))) {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
http_port: Number(e.target.value),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="ml-2 text-white w-[150px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<span>Input Audio Device:</span>
|
||||||
|
<Select
|
||||||
|
variant="standard"
|
||||||
|
name="inputDevice"
|
||||||
|
value={settings.input_device.index}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newDevice = inputDevices.find(
|
||||||
|
(dev) => dev.index === Number(e.target.value),
|
||||||
|
);
|
||||||
|
console.log('Selected input device index:', newDevice);
|
||||||
|
if (newDevice) {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
input_device: newDevice,
|
||||||
|
}));
|
||||||
|
sendSettingsToBackend({
|
||||||
|
...settings,
|
||||||
|
input_device: newDevice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="ml-2 w-64"
|
||||||
|
>
|
||||||
|
{inputDevices.map((dev) => (
|
||||||
|
<MenuItem key={dev.index} value={dev.index}>
|
||||||
|
{dev.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<span>Output Audio Device:</span>
|
||||||
|
<Select
|
||||||
|
variant="standard"
|
||||||
|
name="outputDevice"
|
||||||
|
value={settings.output_device.index}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newDevice = outputDevices.find(
|
||||||
|
(dev) => dev.index === Number(e.target.value),
|
||||||
|
);
|
||||||
|
if (newDevice) {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
output_device: newDevice,
|
||||||
|
}));
|
||||||
|
sendSettingsToBackend({
|
||||||
|
...settings,
|
||||||
|
output_device: newDevice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="ml-2 w-64"
|
||||||
|
>
|
||||||
|
{outputDevices.map((dev) => (
|
||||||
|
<MenuItem key={dev.index} value={dev.index}>
|
||||||
|
{dev.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<span>Recording Length (seconds):</span>
|
||||||
|
<TextField
|
||||||
|
variant="standard"
|
||||||
|
type="text"
|
||||||
|
name="recordingLength"
|
||||||
|
value={settings.recording_length}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!Number.isNaN(Number(e.target.value))) {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
recording_length: Number(e.target.value),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => handleChange()}
|
||||||
|
className="ml-2 w-[150px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<span>Clip Output Folder:</span>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<TextField
|
||||||
|
variant="standard"
|
||||||
|
type="text"
|
||||||
|
name="savePath"
|
||||||
|
value={settings.save_path}
|
||||||
|
className="ml-2 w-[300px]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFolderChange}
|
||||||
|
className="ml-2 px-3 py-1 rounded bg-plumDark text-offwhite hover:bg-plum"
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
electron-ui/src/renderer/api.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export const getBaseUrl = async () => {
|
||||||
|
const port = await window.audio.getPort();
|
||||||
|
if (port.error || !port.port) {
|
||||||
|
return `http://localhost:5010`;
|
||||||
|
}
|
||||||
|
// You can store the base URL in localStorage, a config file, or state
|
||||||
|
return `http://localhost:${port.port}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function apiFetch(endpoint: string, options = {}) {
|
||||||
|
const url = `${await getBaseUrl()}/${endpoint}`;
|
||||||
|
return fetch(url, options);
|
||||||
|
}
|
||||||
@ -1,415 +0,0 @@
|
|||||||
import React, {
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
useCallback,
|
|
||||||
useRef,
|
|
||||||
} 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 { useWavesurfer } from '@wavesurfer/react';
|
|
||||||
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.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 { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { ClipMetadata } from '../../redux/types';
|
|
||||||
import { useAppSelector } from '../hooks';
|
|
||||||
|
|
||||||
export interface AudioTrimmerProps {
|
|
||||||
metadata: ClipMetadata;
|
|
||||||
onSave?: (metadata: ClipMetadata) => void;
|
|
||||||
onDelete?: (metadata: ClipMetadata) => void;
|
|
||||||
onMove?: (newCollection: string, metadata: ClipMetadata) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AudioTrimmer({
|
|
||||||
metadata,
|
|
||||||
onSave,
|
|
||||||
onDelete,
|
|
||||||
onMove,
|
|
||||||
}: AudioTrimmerProps) {
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
||||||
useSortable({ id: metadata.filename });
|
|
||||||
|
|
||||||
// Dialog state for editing name
|
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
||||||
const [nameInput, setNameInput] = useState<string>(metadata.name);
|
|
||||||
const collectionNames = useAppSelector((state) =>
|
|
||||||
state.collections.map((col) => col.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNameInput(metadata.name);
|
|
||||||
}, [metadata.name]);
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
closeEditDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
|
||||||
const containerRef = useRef(null);
|
|
||||||
// const [clipStart, setClipStart] = useState<number | undefined>(undefined);
|
|
||||||
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
const plugins = useMemo(
|
|
||||||
() => [
|
|
||||||
RegionsPlugin.create(),
|
|
||||||
ZoomPlugin.create({
|
|
||||||
scale: 0.25,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileBaseName =
|
|
||||||
metadata.filename.split('\\').pop()?.split('/').pop() || 'Unknown';
|
|
||||||
|
|
||||||
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
|
||||||
container: containerRef,
|
|
||||||
height: 100,
|
|
||||||
waveColor: '#ccb1ff',
|
|
||||||
progressColor: '#6e44ba',
|
|
||||||
hideScrollbar: true,
|
|
||||||
url: blobUrl,
|
|
||||||
plugins,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add this ref to always have the latest metadata
|
|
||||||
const metadataRef = useRef(metadata);
|
|
||||||
useEffect(() => {
|
|
||||||
metadataRef.current = metadata;
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
const onRegionCreated = useCallback(
|
|
||||||
(newRegion: any) => {
|
|
||||||
if (wavesurfer === null) return;
|
|
||||||
|
|
||||||
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
|
||||||
let isNew = metadataRef.current.startTime === undefined;
|
|
||||||
|
|
||||||
allRegions.forEach((region) => {
|
|
||||||
if (region.id !== newRegion.id) {
|
|
||||||
if (
|
|
||||||
region.start === newRegion.start &&
|
|
||||||
region.end === newRegion.end
|
|
||||||
) {
|
|
||||||
newRegion.remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
region.remove();
|
|
||||||
isNew = !(region.start === 0 && region.end === 0);
|
|
||||||
// console.log('Region replace:', newRegion, region);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isNew) {
|
|
||||||
console.log('Region created:', metadataRef.current);
|
|
||||||
const updated = {
|
|
||||||
...metadataRef.current,
|
|
||||||
startTime: newRegion.start,
|
|
||||||
endTime: newRegion.end,
|
|
||||||
};
|
|
||||||
if (onSave) {
|
|
||||||
onSave(updated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[plugins, wavesurfer, onSave],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRegionUpdated = useCallback(
|
|
||||||
(newRegion: any) => {
|
|
||||||
if (wavesurfer === null) return;
|
|
||||||
|
|
||||||
const updated = {
|
|
||||||
...metadataRef.current,
|
|
||||||
startTime: newRegion.start,
|
|
||||||
endTime: newRegion.end,
|
|
||||||
};
|
|
||||||
if (onSave) {
|
|
||||||
onSave(updated);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onSave, wavesurfer],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const plugin = plugins[0] as RegionsPlugin;
|
|
||||||
|
|
||||||
if (!isReady) return;
|
|
||||||
// console.log('ready, setting up regions plugin', plugin, isReady);
|
|
||||||
if (
|
|
||||||
metadataRef.current.startTime !== undefined &&
|
|
||||||
metadataRef.current.endTime !== undefined
|
|
||||||
) {
|
|
||||||
// setClipStart(metadata.startTime);
|
|
||||||
// setClipEnd(metadata.endTime);
|
|
||||||
// console.log('Adding region from metadata:', metadata);=
|
|
||||||
|
|
||||||
const allRegions = plugin.getRegions();
|
|
||||||
// console.log('Existing regions:', allRegions);
|
|
||||||
if (
|
|
||||||
allRegions.length === 0 ||
|
|
||||||
(allRegions.length === 1 &&
|
|
||||||
allRegions[0].start === 0 &&
|
|
||||||
allRegions[0].end === 0)
|
|
||||||
) {
|
|
||||||
// console.log('adding region from metadata:', metadataRef.current);
|
|
||||||
plugin.addRegion({
|
|
||||||
start: metadataRef.current.startTime,
|
|
||||||
end: metadataRef.current.endTime,
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// setClipStart(0);
|
|
||||||
// setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
|
||||||
}
|
|
||||||
}, [isReady, plugins]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const plugin = plugins[0] as RegionsPlugin;
|
|
||||||
plugin.unAll();
|
|
||||||
plugin.on('region-created', onRegionCreated);
|
|
||||||
plugin.on('region-updated', onRegionUpdated);
|
|
||||||
plugin.enableDragSelection({
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
});
|
|
||||||
}, [onRegionCreated, onRegionUpdated, plugins]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let url: string | null = null;
|
|
||||||
async function fetchAudio() {
|
|
||||||
// console.log('Loading audio buffer for file:', filename);
|
|
||||||
const buffer = await window.audio.loadAudioBuffer(metadata.filename);
|
|
||||||
// console.log('Received buffer:', buffer.buffer);
|
|
||||||
if (buffer.buffer && !buffer.error) {
|
|
||||||
const audioData = buffer.buffer
|
|
||||||
? new Uint8Array(buffer.buffer)
|
|
||||||
: buffer;
|
|
||||||
url = URL.createObjectURL(new Blob([audioData]));
|
|
||||||
// console.log('Created blob URL:', url);
|
|
||||||
setBlobUrl(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchAudio();
|
|
||||||
return () => {
|
|
||||||
if (url) URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
}, [metadata.filename]);
|
|
||||||
|
|
||||||
const onPlayPause = () => {
|
|
||||||
if (wavesurfer === null) return;
|
|
||||||
if (isPlaying) {
|
|
||||||
wavesurfer.pause();
|
|
||||||
} else {
|
|
||||||
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
|
||||||
if (allRegions.length > 0) {
|
|
||||||
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
|
||||||
} else {
|
|
||||||
wavesurfer.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const secs = (seconds % 60).toFixed(0);
|
|
||||||
return `${minutes}:${secs.padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={setNodeRef}
|
|
||||||
style={{
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
position: 'relative',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
}}
|
|
||||||
className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
{...attributes}
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
{...listeners}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: '10px',
|
|
||||||
borderRadius: '5px 0 0 5px',
|
|
||||||
cursor: 'grab',
|
|
||||||
}}
|
|
||||||
className="bg-neutral-800"
|
|
||||||
/>
|
|
||||||
{/* <div className="flex flex-col"> */}
|
|
||||||
<div className="ml-4 mr-2 p-2">
|
|
||||||
<div className="grid justify-items-stretch grid-cols-2">
|
|
||||||
<div className="mb-5px flex flex-col">
|
|
||||||
<span
|
|
||||||
className="font-bold text-lg text-white mb-1 cursor-pointer"
|
|
||||||
onClick={openEditDialog}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault();
|
|
||||||
openEditDialog();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title="Click to edit name"
|
|
||||||
tabIndex={0}
|
|
||||||
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' } },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 className="m-1 wavesurfer-scroll-container">
|
|
||||||
<div ref={containerRef} className="wavesurfer-inner" />
|
|
||||||
</div>
|
|
||||||
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
|
||||||
<div className="m-1 flex justify-start">
|
|
||||||
<text className="text-sm ">
|
|
||||||
Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
|
|
||||||
{formatTime(metadata.endTime ?? 0)}
|
|
||||||
</text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -12,9 +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';
|
||||||
|
|
||||||
export interface ClipListProps {
|
export interface ClipListProps {
|
||||||
collection: string;
|
collection: string;
|
||||||
@ -31,6 +32,33 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log('Files dropped:', event.dataTransfer.files);
|
||||||
|
const files = Array.from(event.dataTransfer.files).filter((file) =>
|
||||||
|
file.type.startsWith('audio/'),
|
||||||
|
);
|
||||||
|
if (files.length > 0) {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach((file) => formData.append('files', file));
|
||||||
|
|
||||||
|
// todo send the file to the backend and add to the collection
|
||||||
|
|
||||||
|
// fetch('http://localhost:5010/file/upload', {
|
||||||
|
// method: 'POST',
|
||||||
|
// body: formData,
|
||||||
|
// })
|
||||||
|
// .then((res) => res.json())
|
||||||
|
// .catch((err) => console.error('Error uploading files:', err));
|
||||||
|
// Implement your onDrop logic here
|
||||||
|
// onDrop(files, selectedCollection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
async function handleDragEnd(event: any) {
|
async function handleDragEnd(event: any) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (active.id !== over?.id) {
|
if (active.id !== over?.id) {
|
||||||
@ -50,19 +78,16 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
payload: { collection, newMetadata },
|
payload: { collection, newMetadata },
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await apiFetch('meta/collection/clips/reorder', {
|
||||||
'http://localhost:5010/meta/collection/clips/reorder',
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': 'application/json',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: collection,
|
|
||||||
clips: newMetadata.clips,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
);
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clips: newMetadata.clips,
|
||||||
|
}),
|
||||||
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('handle reorder return:', data.collections);
|
console.log('handle reorder return:', data.collections);
|
||||||
dispatch({ type: 'metadata/setAllData', payload: data });
|
dispatch({ type: 'metadata/setAllData', payload: data });
|
||||||
@ -78,7 +103,7 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
type: 'metadata/deleteClip',
|
type: 'metadata/deleteClip',
|
||||||
payload: { collection, clip: meta },
|
payload: { collection, clip: meta },
|
||||||
});
|
});
|
||||||
fetch('http://localhost:5010/meta/collection/clips/remove', {
|
apiFetch('meta/collection/clips/remove', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -99,7 +124,7 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
type: 'metadata/moveClip',
|
type: 'metadata/moveClip',
|
||||||
payload: { sourceCollection: collection, targetCollection, clip: meta },
|
payload: { sourceCollection: collection, targetCollection, clip: meta },
|
||||||
});
|
});
|
||||||
fetch('http://localhost:5010/meta/collection/clips/move', {
|
apiFetch('meta/collection/clips/move', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -120,19 +145,16 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
type: 'metadata/editClip',
|
type: 'metadata/editClip',
|
||||||
payload: { collection, clip: meta },
|
payload: { collection, clip: meta },
|
||||||
});
|
});
|
||||||
const response = await fetch(
|
const response = await apiFetch('meta/collection/clips/edit', {
|
||||||
'http://localhost:5010/meta/collection/clips/edit',
|
method: 'POST',
|
||||||
{
|
headers: {
|
||||||
method: 'POST',
|
'Content-Type': 'application/json',
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: collection,
|
|
||||||
clip: meta,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
);
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clip: meta,
|
||||||
|
}),
|
||||||
|
});
|
||||||
await response.json();
|
await response.json();
|
||||||
// console.log('handle clip save return:', data.collections);
|
// console.log('handle clip save return:', data.collections);
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -145,7 +167,11 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-full flex flex-col justify-start bg-midnight text-offwhite">
|
<div
|
||||||
|
className="min-h-full flex flex-col justify-start bg-midnight text-offwhite"
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
|
|||||||
367
electron-ui/src/renderer/components/Trimmer/AudioTrimer.tsx
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import Slider from '@mui/material/Slider';
|
||||||
|
import ToggleButton from '@mui/material/ToggleButton';
|
||||||
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||||
|
import { useWavesurfer } from '@wavesurfer/react';
|
||||||
|
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||||
|
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { ClipMetadata, PlaybackType } from '../../../redux/types';
|
||||||
|
import { useAppSelector } from '../../hooks';
|
||||||
|
import PlayStopIcon from '../icons/playStopIcon';
|
||||||
|
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 {
|
||||||
|
metadata: ClipMetadata;
|
||||||
|
onSave?: (metadata: ClipMetadata) => void;
|
||||||
|
onDelete?: (metadata: ClipMetadata) => void;
|
||||||
|
onMove?: (newCollection: string, metadata: ClipMetadata) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AudioTrimmer({
|
||||||
|
metadata,
|
||||||
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
onMove,
|
||||||
|
}: AudioTrimmerProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id: metadata.filename });
|
||||||
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [volumeInput, setVolumeInput] = useState<number>(metadata.volume ?? 1);
|
||||||
|
const collectionNames = useAppSelector((state) =>
|
||||||
|
state.collections.map((col) => col.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDialogSave = (newName: string) => {
|
||||||
|
if (newName.trim() && newName !== metadata.name) {
|
||||||
|
const updated = { ...metadata, name: newName.trim() };
|
||||||
|
if (onSave) onSave(updated);
|
||||||
|
}
|
||||||
|
setEditDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
// const [clipStart, setClipStart] = useState<number | undefined>(undefined);
|
||||||
|
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const plugins = useMemo(
|
||||||
|
() => [
|
||||||
|
RegionsPlugin.create(),
|
||||||
|
ZoomPlugin.create({
|
||||||
|
scale: 0.25,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||||
|
container: containerRef,
|
||||||
|
height: 100,
|
||||||
|
waveColor: '#ccb1ff',
|
||||||
|
progressColor: '#6e44ba',
|
||||||
|
hideScrollbar: true,
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add this ref to always have the latest metadata
|
||||||
|
const metadataRef = useRef(metadata);
|
||||||
|
useEffect(() => {
|
||||||
|
metadataRef.current = metadata;
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
|
const onRegionCreated = useCallback(
|
||||||
|
(newRegion: any) => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
|
||||||
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
|
let isNew = metadataRef.current.startTime === undefined;
|
||||||
|
|
||||||
|
allRegions.forEach((region) => {
|
||||||
|
if (region.id !== newRegion.id) {
|
||||||
|
if (
|
||||||
|
region.start === newRegion.start &&
|
||||||
|
region.end === newRegion.end
|
||||||
|
) {
|
||||||
|
newRegion.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
region.remove();
|
||||||
|
isNew = !(region.start === 0 && region.end === 0);
|
||||||
|
// console.log('Region replace:', newRegion, region);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
console.log('Region created:', metadataRef.current);
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[plugins, wavesurfer, onSave],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRegionUpdated = useCallback(
|
||||||
|
(newRegion: any) => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSave, wavesurfer],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
|
|
||||||
|
if (!isReady) return;
|
||||||
|
// console.log('ready, setting up regions plugin', plugin, isReady);
|
||||||
|
if (
|
||||||
|
metadataRef.current.startTime !== undefined &&
|
||||||
|
metadataRef.current.endTime !== undefined
|
||||||
|
) {
|
||||||
|
// setClipStart(metadata.startTime);
|
||||||
|
// setClipEnd(metadata.endTime);
|
||||||
|
// console.log('Adding region from metadata:', metadata);=
|
||||||
|
|
||||||
|
const allRegions = plugin.getRegions();
|
||||||
|
// console.log('Existing regions:', allRegions);
|
||||||
|
if (
|
||||||
|
allRegions.length === 0 ||
|
||||||
|
(allRegions.length === 1 &&
|
||||||
|
allRegions[0].start === 0 &&
|
||||||
|
allRegions[0].end === 0)
|
||||||
|
) {
|
||||||
|
// console.log('adding region from metadata:', metadataRef.current);
|
||||||
|
plugin.addRegion({
|
||||||
|
start: metadataRef.current.startTime,
|
||||||
|
end: metadataRef.current.endTime,
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
drag: false,
|
||||||
|
resize: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// setClipStart(0);
|
||||||
|
// setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
||||||
|
}
|
||||||
|
}, [isReady, plugins]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
|
plugin.unAll();
|
||||||
|
plugin.on('region-created', onRegionCreated);
|
||||||
|
plugin.on('region-updated', onRegionUpdated);
|
||||||
|
plugin.enableDragSelection({
|
||||||
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
|
});
|
||||||
|
}, [onRegionCreated, onRegionUpdated, plugins]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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() {
|
||||||
|
const buffer = await window.audio.loadAudioBuffer(metadata.filename);
|
||||||
|
if (cancelled) return;
|
||||||
|
if (buffer.buffer && !buffer.error) {
|
||||||
|
const audioData = buffer.buffer
|
||||||
|
? new Uint8Array(buffer.buffer)
|
||||||
|
: buffer;
|
||||||
|
wavesurfer?.loadBlob(new Blob([audioData]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchAudio();
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isVisible, metadata.filename, wavesurfer]);
|
||||||
|
|
||||||
|
const onPlayPause = () => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
if (isPlaying) {
|
||||||
|
wavesurfer.pause();
|
||||||
|
} else {
|
||||||
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
|
if (allRegions.length > 0) {
|
||||||
|
wavesurfer.setVolume(metadata.volume ?? 1);
|
||||||
|
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
||||||
|
} else {
|
||||||
|
wavesurfer.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const secs = (seconds % 60).toFixed(0);
|
||||||
|
return `${minutes}:${secs.padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={(el) => {
|
||||||
|
setNodeRef(el);
|
||||||
|
rootRef.current = el;
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
}}
|
||||||
|
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
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...attributes}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '10px',
|
||||||
|
borderRadius: '5px 0 0 5px',
|
||||||
|
cursor: 'grab',
|
||||||
|
}}
|
||||||
|
className="bg-neutral-800"
|
||||||
|
/>
|
||||||
|
{/* <div className="flex flex-col"> */}
|
||||||
|
<div className="ml-4 mr-2 p-2">
|
||||||
|
<div className="grid justify-items-stretch grid-cols-2">
|
||||||
|
<TitleBlock
|
||||||
|
name={metadata.name}
|
||||||
|
filename={metadata.filename}
|
||||||
|
onNameClick={() => setEditDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
<ClipButtonRow
|
||||||
|
isPlaying={isPlaying}
|
||||||
|
collectionNames={collectionNames}
|
||||||
|
onPlayPause={onPlayPause}
|
||||||
|
onMove={(collectionName) => {
|
||||||
|
if (onMove !== undefined) {
|
||||||
|
onMove(collectionName, metadata);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDelete={() => setDeleteDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="m-1 wavesurfer-scroll-container">
|
||||||
|
<div ref={containerRef} className="wavesurfer-inner" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-2">
|
||||||
|
<span className="w-1/5 flex-none text-sm text-neutral-500 self-center">
|
||||||
|
Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
|
||||||
|
{formatTime(metadata.endTime ?? 0)}
|
||||||
|
</span>
|
||||||
|
<div className="w-3/5 flex-1 flex justify-center items-center">
|
||||||
|
<Slider
|
||||||
|
value={volumeInput}
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
onChange={(e, newValue) => setVolumeInput(newValue as number)}
|
||||||
|
onChangeCommitted={(e, newValue) => {
|
||||||
|
const newVolume = newValue as number;
|
||||||
|
console.log('Volume change:', newVolume);
|
||||||
|
if (onSave) onSave({ ...metadata, volume: newVolume });
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
|
className="p-0 m-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/5 flex justify-end text-sm text-neutral-500">
|
||||||
|
<ToggleButtonGroup value={metadata.playbackType}>
|
||||||
|
<ToggleButton
|
||||||
|
value="playStop"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onSave)
|
||||||
|
onSave({
|
||||||
|
...metadata,
|
||||||
|
playbackType: PlaybackType.PlayStop,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayStopIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="playOverlap"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
if (onSave)
|
||||||
|
onSave({
|
||||||
|
...metadata,
|
||||||
|
playbackType: PlaybackType.PlayOverlap,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayOverlapIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
electron-ui/src/renderer/components/Trimmer/TitleBlock.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
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}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
} 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>
|
||||||
|
<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={input}
|
||||||
|
onChange={(e) => {
|
||||||
|
setInput(e.target.value);
|
||||||
|
}}
|
||||||
|
rows={3}
|
||||||
|
onFocus={(event) => event.target.select()}
|
||||||
|
aria-label="Edit clip name"
|
||||||
|
style={{ minHeight: '3em' }}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function PlayOverlapIcon({
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor',
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 32 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
{/* Filled play arrow */}
|
||||||
|
<polygon points="4,4 4,20 16,12" fill={color} />
|
||||||
|
{/* Outlined play arrow (underneath and to the right) */}
|
||||||
|
<polygon
|
||||||
|
points="12,4 12,20 24,12"
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
electron-ui/src/renderer/components/icons/playStopIcon.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export default function PlayStopIcon({
|
||||||
|
size = 24,
|
||||||
|
color = 'currentColor',
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
color?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 48 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-label="Play/Stop Icon"
|
||||||
|
>
|
||||||
|
{/* Play Arrow */}
|
||||||
|
<polygon points="4,4 20,12 4,20" fill={color} />
|
||||||
|
{/* Stop Square */}
|
||||||
|
<rect x="28" y="4" width="16" height="16" rx="2" fill={color} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
4
electron-ui/src/renderer/preload.d.ts
vendored
@ -1,10 +1,10 @@
|
|||||||
import { ElectronHandler, FileHandler } from '../main/preload';
|
import { ElectronHandler, AudioHandler } from '../main/preload';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronHandler;
|
electron: ElectronHandler;
|
||||||
audio: FileHandler;
|
audio: AudioHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
421
electron-ui/test_meta.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
3
stream_deck_plugin/.gitignore
vendored
@ -4,4 +4,5 @@ packages/
|
|||||||
ClipTrimDotNet/bin/
|
ClipTrimDotNet/bin/
|
||||||
ClipTrimDotNet/obj/
|
ClipTrimDotNet/obj/
|
||||||
ClipTrimDotNet/dist/
|
ClipTrimDotNet/dist/
|
||||||
ClipTrimDotNet/node_modules/
|
ClipTrimDotNet/node_modules/
|
||||||
|
.vs/
|
||||||
@ -1,96 +0,0 @@
|
|||||||
{
|
|
||||||
"Version": 1,
|
|
||||||
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
|
|
||||||
"Documents": [
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"DocumentGroupContainers": [
|
|
||||||
{
|
|
||||||
"Orientation": 0,
|
|
||||||
"VerticalTabListWidth": 256,
|
|
||||||
"DocumentGroups": [
|
|
||||||
{
|
|
||||||
"DockedWidth": 297,
|
|
||||||
"SelectedChildIndex": 2,
|
|
||||||
"Children": [
|
|
||||||
{
|
|
||||||
"$type": "Bookmark",
|
|
||||||
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 2,
|
|
||||||
"Title": "ProfileSwitcher.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"ViewState": "AgIAAFkAAAAAAAAAAAAlwG8AAABKAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:06:24.045Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 0,
|
|
||||||
"Title": "ClipTrimClient.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGMAAAAJAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:03:49.814Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 3,
|
|
||||||
"Title": "CollectionMetaData.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:03:47.862Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 1,
|
|
||||||
"Title": "Player.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs*",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Player.cs*",
|
|
||||||
"ViewState": "AgIAAHIAAAAAAAAAAAA3wIYAAABMAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:00:23.762Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Bookmark",
|
|
||||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
{
|
|
||||||
"Version": 1,
|
|
||||||
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
|
|
||||||
"Documents": [
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
|
||||||
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"DocumentGroupContainers": [
|
|
||||||
{
|
|
||||||
"Orientation": 0,
|
|
||||||
"VerticalTabListWidth": 256,
|
|
||||||
"DocumentGroups": [
|
|
||||||
{
|
|
||||||
"DockedWidth": 297,
|
|
||||||
"SelectedChildIndex": 1,
|
|
||||||
"Children": [
|
|
||||||
{
|
|
||||||
"$type": "Bookmark",
|
|
||||||
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 0,
|
|
||||||
"Title": "WavPlayer.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\WavPlayer.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\WavPlayer.cs",
|
|
||||||
"ViewState": "AgIAALYAAAAAAAAAAAAAALsAAAANAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:16:26.477Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 2,
|
|
||||||
"Title": "ProfileSwitcher.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
|
||||||
"ViewState": "AgIAAG8AAAAAAAAAAAAWwG8AAABKAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:06:24.045Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 3,
|
|
||||||
"Title": "ClipTrimClient.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
|
||||||
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGIAAAApAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:03:49.814Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 4,
|
|
||||||
"Title": "CollectionMetaData.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
|
||||||
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:03:47.862Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Document",
|
|
||||||
"DocumentIndex": 1,
|
|
||||||
"Title": "Player.cs",
|
|
||||||
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
|
||||||
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
|
|
||||||
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
|
||||||
"RelativeToolTip": "ClipTrimDotNet\\Player.cs",
|
|
||||||
"ViewState": "AgIAAHoAAAAAAAAAAAAswIwAAAAbAAAAAAAAAA==",
|
|
||||||
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
|
||||||
"WhenOpened": "2026-02-21T15:00:23.762Z",
|
|
||||||
"EditorCaption": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"$type": "Bookmark",
|
|
||||||
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,25 +1,25 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.8.34330.188
|
VisualStudioVersion = 17.8.34330.188
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClipTrimDotNet", "ClipTrimDotNet\ClipTrimDotNet.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClipTrimDotNet", "ClipTrimDotNet\ClipTrimDotNet.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {926C6896-F36A-4F3B-A9DD-CA3AA48AD99F}
|
SolutionGuid = {926C6896-F36A-4F3B-A9DD-CA3AA48AD99F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
To use:
|
To use:
|
||||||
1. Right click the project and choose "Manage Nuget Packages"
|
1. Right click the project and choose "Manage Nuget Packages"
|
||||||
2. Choose the restore option in the Nuget screen (or just install the latest StreamDeck-Tools from Nuget)
|
2. Choose the restore option in the Nuget screen (or just install the latest StreamDeck-Tools from Nuget)
|
||||||
3. Update the manifest.json file with the correct details about your plugin
|
3. Update the manifest.json file with the correct details about your plugin
|
||||||
4. Modify PluginAction.cs as needed (it holds the logic for your plugin)
|
4. Modify PluginAction.cs as needed (it holds the logic for your plugin)
|
||||||
5. Modify the PropertyInspector\PluginActionPI.html and PropertyInspector\PluginActionPI.js as needed to show field in the Property Inspector
|
5. Modify the PropertyInspector\PluginActionPI.html and PropertyInspector\PluginActionPI.js as needed to show field in the Property Inspector
|
||||||
6. Before releasing, change the Assembly Information (Right click the project -> Properties -> Application -> Assembly Information...)
|
6. Before releasing, change the Assembly Information (Right click the project -> Properties -> Application -> Assembly Information...)
|
||||||
|
|
||||||
For help with StreamDeck-Tools:
|
For help with StreamDeck-Tools:
|
||||||
Discord Server: http://discord.barraider.com
|
Discord Server: http://discord.barraider.com
|
||||||
Resources:
|
Resources:
|
||||||
* StreamDeck-Tools samples and tutorial: https://github.com/BarRaider/streamdeck-tools
|
* StreamDeck-Tools samples and tutorial: https://github.com/BarRaider/streamdeck-tools
|
||||||
* EasyPI library (for working with Property Inspector): https://github.com/BarRaider/streamdeck-easypi
|
* EasyPI library (for working with Property Inspector): https://github.com/BarRaider/streamdeck-easypi
|
||||||
|
|
||||||
|
|||||||
@ -1,22 +1,42 @@
|
|||||||
<?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>
|
||||||
</assemblyBinding>
|
<dependentAssembly>
|
||||||
</runtime>
|
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
|
||||||
</configuration>
|
<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>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
|
|
||||||
|
|||||||
@ -1,42 +1,53 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Serialization;
|
||||||
using System;
|
using Newtonsoft.Json.Converters;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System;
|
||||||
using System.Text;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
namespace ClipTrimDotNet.Client
|
using System.Threading.Tasks;
|
||||||
{
|
using System.Text.Json.Serialization;
|
||||||
public enum PlaybackType
|
|
||||||
{
|
namespace ClipTrimDotNet.Client
|
||||||
playStop,
|
{
|
||||||
playOverlap
|
public enum PlaybackType
|
||||||
}
|
{
|
||||||
public class ClipMetadata
|
playStop,
|
||||||
{
|
playOverlap
|
||||||
[JsonProperty(PropertyName = "filename")]
|
}
|
||||||
public string Filename { get; set; }
|
public class ClipMetadata
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "filename")]
|
||||||
[JsonProperty(PropertyName = "name")]
|
[JsonPropertyName("filename")]
|
||||||
public string Name { get; set; }
|
|
||||||
|
public string Filename { get; set; }
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "volume")]
|
|
||||||
public double Volume { get; set; } = 1.0;
|
[JsonProperty(PropertyName = "name")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
[JsonProperty(PropertyName = "startTime")]
|
|
||||||
public double StartTime { get; set; } = 0.0;
|
|
||||||
|
[JsonProperty(PropertyName = "volume")]
|
||||||
|
[JsonPropertyName("volume")]
|
||||||
[JsonProperty(PropertyName = "endTime")]
|
public double Volume { get; set; } = 1.0;
|
||||||
public double EndTime { get; set; } = 0.0;
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "startTime")]
|
||||||
[JsonProperty(PropertyName = "playbackType")]
|
[JsonPropertyName("startTime")]
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
public double StartTime { get; set; } = 0.0;
|
||||||
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
|
|
||||||
}
|
|
||||||
}
|
[JsonProperty(PropertyName = "endTime")]
|
||||||
|
[JsonPropertyName("endTime")]
|
||||||
|
public double EndTime { get; set; } = 0.0;
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "playbackType")]
|
||||||
|
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
[JsonPropertyName("playbackType")]
|
||||||
|
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,110 +1,287 @@
|
|||||||
using System;
|
using BarRaider.SdTools;
|
||||||
using System.Collections.Generic;
|
using ClipTrimDotNet.Keys;
|
||||||
using System.Linq;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
using System.Net.Http;
|
using Newtonsoft.Json;
|
||||||
using System.Text;
|
using SocketIOClient;
|
||||||
using System.Threading.Tasks;
|
using System;
|
||||||
using Newtonsoft.Json;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
namespace ClipTrimDotNet.Client
|
using System.Net.Http;
|
||||||
{
|
using System.Runtime.CompilerServices;
|
||||||
public class ClipTrimClient
|
using System.Text;
|
||||||
{
|
using System.Threading.Tasks;
|
||||||
private static ClipTrimClient? instance;
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
public static ClipTrimClient Instance
|
|
||||||
{
|
namespace ClipTrimDotNet.Client
|
||||||
get
|
{
|
||||||
{
|
public class ClipTrimClient
|
||||||
if (instance == null)
|
{
|
||||||
{
|
private static ClipTrimClient? instance;
|
||||||
instance = new ClipTrimClient();
|
public static ClipTrimClient Instance
|
||||||
}
|
{
|
||||||
return instance;
|
get
|
||||||
}
|
{
|
||||||
}
|
if (instance == null)
|
||||||
|
{
|
||||||
private HttpClient httpClient;
|
instance = new ClipTrimClient();
|
||||||
|
}
|
||||||
public ClipTrimClient()
|
return instance;
|
||||||
{
|
}
|
||||||
httpClient = new HttpClient()
|
}
|
||||||
{
|
|
||||||
BaseAddress = new Uri("http://localhost:5010/"),
|
//private HttpClient httpClient;
|
||||||
Timeout = TimeSpan.FromSeconds(10)
|
private SocketIO? socket;
|
||||||
};
|
|
||||||
Task.Run(ShortPoll);
|
public string HostName
|
||||||
}
|
{
|
||||||
|
get
|
||||||
public async Task ShortPoll()
|
{
|
||||||
{
|
//return $"http://localhost:5010/";
|
||||||
while (true)
|
return $"http://localhost:{GlobalSettings.Instance.PortNumber}/";
|
||||||
{
|
}
|
||||||
await GetMetadata();
|
}
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5));
|
|
||||||
|
private string? currentHostname = null;
|
||||||
}
|
|
||||||
}
|
void CreateSocket()
|
||||||
|
{
|
||||||
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"Starting ClipTrimClient on port {HostName}");
|
||||||
public CollectionMetaData? SelectedCollection { get; private set; }
|
socket = new SocketIO(new Uri(HostName));
|
||||||
public int PageIndex { get; private set; } = 0;
|
currentHostname = HostName;
|
||||||
private async Task GetMetadata()
|
socket.Options.AutoUpgrade = false;
|
||||||
{
|
//socket.Options.Path = "/socket.io";
|
||||||
try
|
socket.Options.ConnectionTimeout = TimeSpan.FromSeconds(10);
|
||||||
{
|
socket.Options.Reconnection = true;
|
||||||
var response = await httpClient.GetAsync("meta");
|
socket.On("full_data", ctx =>
|
||||||
if (response.IsSuccessStatusCode)
|
{
|
||||||
{
|
try
|
||||||
var json = await response.Content.ReadAsStringAsync();
|
{
|
||||||
dynamic collections = JsonConvert.DeserializeObject(json);
|
var response = ctx.GetValue<List<CollectionMetaData>>(0);
|
||||||
collections = collections.collections;
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data event {JsonConvert.SerializeObject(response)}");
|
||||||
Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString());
|
Collections = response!;
|
||||||
}
|
Player.TickAll();
|
||||||
}
|
PageNavigator.TickAll();
|
||||||
catch (Exception ex)
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Collections {JsonConvert.SerializeObject(Collections)}");
|
||||||
{
|
}
|
||||||
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}");
|
catch (Exception ex)
|
||||||
return;
|
{
|
||||||
}
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data error {ex.ToString()}");
|
||||||
|
}
|
||||||
}
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
public List<string> GetCollectionNames()
|
socket.On("collection_updated", ctx =>
|
||||||
{
|
{
|
||||||
//await GetMetadata();
|
try
|
||||||
return Collections.Select(x => x.Name).ToList();
|
{
|
||||||
}
|
var response = ctx.GetValue<CollectionMetaData>(0)!;
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"collection_updated event {JsonConvert.SerializeObject(response)}");
|
||||||
public void SetSelectedCollectionByName(string name)
|
int index = Collections.FindIndex(x => x.Id == response.Id);
|
||||||
{
|
if (index != -1)
|
||||||
var collection = Collections.FirstOrDefault(x => x.Name == name);
|
{
|
||||||
if (collection != null)
|
Collections[index] = response;
|
||||||
{
|
Player.TickAll();
|
||||||
SelectedCollection = collection;
|
PageNavigator.TickAll();
|
||||||
PageIndex = 0;
|
}
|
||||||
}
|
}
|
||||||
}
|
catch
|
||||||
|
{
|
||||||
public ClipMetadata? GetClipByPagedIndex(int index)
|
|
||||||
{
|
}
|
||||||
if (SelectedCollection == null) return null;
|
|
||||||
int clipIndex = PageIndex * 10 + index;
|
return Task.CompletedTask;
|
||||||
if (clipIndex >= 0 && clipIndex < SelectedCollection.Clips.Count)
|
});
|
||||||
{
|
|
||||||
return SelectedCollection.Clips[clipIndex];
|
socket.OnConnected += (sender, e) =>
|
||||||
}
|
{
|
||||||
return null;
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket connected: {e}");
|
||||||
}
|
};
|
||||||
|
|
||||||
public async void PlayClip(ClipMetadata? metadata)
|
socket.OnDisconnected += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (metadata == null) return;
|
Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket disconnected: {e}");
|
||||||
|
Task.Run(async () => await Connect());
|
||||||
var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json"));
|
};
|
||||||
if (!response.IsSuccessStatusCode)
|
Task.Run(async () => await Connect());
|
||||||
{
|
}
|
||||||
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}");
|
|
||||||
}
|
public ClipTrimClient()
|
||||||
}
|
{
|
||||||
}
|
//httpClient = new HttpClient()
|
||||||
}
|
//{
|
||||||
|
// BaseAddress = new Uri("http://localhost:5010/"),
|
||||||
|
// Timeout = TimeSpan.FromSeconds(10)
|
||||||
|
//};
|
||||||
|
CreateSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Connect()
|
||||||
|
{
|
||||||
|
if (socket is null) return;
|
||||||
|
while (!socket.Connected)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await socket.ConnectAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
|
||||||
|
public int SelectedCollection { get; private set; } = -1;
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
//await GetMetadata();
|
||||||
|
return Collections.Where(x => x.Name != "Uncategorized").Select(x => x.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentCollectionName()
|
||||||
|
{
|
||||||
|
if (SelectedCollection == -1) return "";
|
||||||
|
return Collections[SelectedCollection].Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetPlayerStringByCoordinateIndex(int index)
|
||||||
|
{
|
||||||
|
if (PageMode)
|
||||||
|
{
|
||||||
|
int pageNumber = index + 1;
|
||||||
|
if(pageNumber <= PageCount)
|
||||||
|
{
|
||||||
|
return pageNumber.ToString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var collection = GetClipByPagedIndex(index);
|
||||||
|
return collection?.Name ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ClipMetadata? GetClipByPagedIndex(int index)
|
||||||
|
{
|
||||||
|
SelectedCollection = Collections.FindIndex(x => x.Name == GlobalSettings.Instance.ProfileName);
|
||||||
|
if (SelectedCollection == -1) return null;
|
||||||
|
int clipIndex = PageIndex * 10 + index;
|
||||||
|
var collection = Collections[SelectedCollection];
|
||||||
|
if (clipIndex >= 0 && clipIndex < collection.Clips.Count)
|
||||||
|
{
|
||||||
|
return collection.Clips[clipIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void PlayClip(int index)
|
||||||
|
{
|
||||||
|
if (PageMode)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
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.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
namespace ClipTrimDotNet.Client
|
|
||||||
{
|
namespace ClipTrimDotNet.Client
|
||||||
public class CollectionMetaData
|
{
|
||||||
{
|
public class CollectionMetaData
|
||||||
[JsonProperty(PropertyName = "name")]
|
{
|
||||||
public string Name { get; set; }
|
[JsonProperty(PropertyName = "name")]
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; }
|
||||||
[JsonProperty(PropertyName = "clips")]
|
|
||||||
public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
|
|
||||||
|
[JsonProperty(PropertyName = "clips")]
|
||||||
|
[JsonPropertyName("clips")]
|
||||||
[JsonProperty(PropertyName = "id")]
|
public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
|
||||||
public int Id { get; set; }
|
|
||||||
}
|
|
||||||
}
|
[JsonProperty(PropertyName = "id")]
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,162 +1,118 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<PropertyGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
<PropertyGroup>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<OutputType>Exe</OutputType>
|
||||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
<LangVersion>10</LangVersion>
|
||||||
<ProjectGuid>{4635D874-69C0-4010-BE46-77EF92EB1553}</ProjectGuid>
|
<Nullable>enable</Nullable>
|
||||||
<OutputType>Exe</OutputType>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>ClipTrimDotNet</RootNamespace>
|
<PreBuildEvent>npm run stop</PreBuildEvent>
|
||||||
<AssemblyName>ClipTrimDotNet</AssemblyName>
|
<PostBuildEvent>npm run start</PostBuildEvent>
|
||||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
<AssemblyTitle>ClipTrimDotNet</AssemblyTitle>
|
||||||
<LangVersion>8</LangVersion>
|
<Product>ClipTrimDotNet</Product>
|
||||||
<FileAlignment>512</FileAlignment>
|
<Copyright>Copyright © 2020</Copyright>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AssemblyVersion>1.0.0.0</AssemblyVersion>
|
||||||
<Deterministic>true</Deterministic>
|
<FileVersion>1.0.0.0</FileVersion>
|
||||||
<Nullable>enable</Nullable>
|
</PropertyGroup>
|
||||||
<TargetFrameworkProfile />
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
</PropertyGroup>
|
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
</PropertyGroup>
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
<DebugSymbols>true</DebugSymbols>
|
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
|
||||||
<DebugType>full</DebugType>
|
</PropertyGroup>
|
||||||
<Optimize>false</Optimize>
|
<ItemGroup>
|
||||||
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
|
<None Remove="Images\app_icon.png" />
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<None Remove="Images\back.png" />
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<None Remove="Images\category_icon.png" />
|
||||||
<WarningLevel>4</WarningLevel>
|
<None Remove="Images\collection.png" />
|
||||||
</PropertyGroup>
|
<None Remove="Images\collection_icon.png" />
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
<None Remove="Images\page_nav.png" />
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<None Remove="Images\page_nav_icon.png" />
|
||||||
<DebugType>pdbonly</DebugType>
|
<None Remove="Images\player.png" />
|
||||||
<Optimize>true</Optimize>
|
<None Remove="Images\player_icon.png" />
|
||||||
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
|
<None Remove="Images\record.png" />
|
||||||
<DefineConstants>TRACE</DefineConstants>
|
<None Remove="Images\record_icon.png" />
|
||||||
<ErrorReport>prompt</ErrorReport>
|
<None Remove="manifest.json" />
|
||||||
<WarningLevel>4</WarningLevel>
|
</ItemGroup>
|
||||||
</PropertyGroup>
|
<ItemGroup>
|
||||||
<ItemGroup>
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<Reference Include="CommandLine, Version=2.9.1.0, Culture=neutral, PublicKeyToken=5a870481e358d379, processorArchitecture=MSIL">
|
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
|
||||||
<HintPath>..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll</HintPath>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||||
</Reference>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||||
<Reference Include="Microsoft.Win32.Registry, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
|
||||||
<HintPath>..\packages\Microsoft.Win32.Registry.4.7.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath>
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
|
||||||
</Reference>
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
|
||||||
<Reference Include="NAudio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.2" />
|
||||||
<HintPath>..\packages\NAudio.2.2.1\lib\net472\NAudio.dll</HintPath>
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
</Reference>
|
<PackageReference Include="NLog" Version="6.0.5" />
|
||||||
<Reference Include="NAudio.Asio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="SocketIOClient" Version="4.0.0.2" />
|
||||||
<HintPath>..\packages\NAudio.Asio.2.2.1\lib\netstandard2.0\NAudio.Asio.dll</HintPath>
|
<PackageReference Include="SocketIOClient.Common" Version="4.0.0" />
|
||||||
</Reference>
|
<PackageReference Include="SocketIOClient.Serializer" Version="4.0.0.1" />
|
||||||
<Reference Include="NAudio.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="SocketIOClient.Serializer.NewtonsoftJson" Version="4.0.0.1" />
|
||||||
<HintPath>..\packages\NAudio.Core.2.2.1\lib\netstandard2.0\NAudio.Core.dll</HintPath>
|
<PackageReference Include="StreamDeck-Tools" Version="6.3.2" />
|
||||||
</Reference>
|
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
|
||||||
<Reference Include="NAudio.Midi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="System.Drawing.Common" Version="9.0.10" />
|
||||||
<HintPath>..\packages\NAudio.Midi.2.2.1\lib\netstandard2.0\NAudio.Midi.dll</HintPath>
|
<PackageReference Include="System.IO.Pipelines" Version="10.0.2" />
|
||||||
</Reference>
|
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
|
||||||
<Reference Include="NAudio.Wasapi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
|
||||||
<HintPath>..\packages\NAudio.Wasapi.2.2.1\lib\netstandard2.0\NAudio.Wasapi.dll</HintPath>
|
<PackageReference Include="System.Text.Encodings.Web" Version="10.0.2" />
|
||||||
</Reference>
|
<PackageReference Include="System.Text.Json" Version="10.0.2" />
|
||||||
<Reference Include="NAudio.WinForms, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
|
||||||
<HintPath>..\packages\NAudio.WinForms.2.2.1\lib\net472\NAudio.WinForms.dll</HintPath>
|
<PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
|
||||||
</Reference>
|
<PackageReference Include="CoreWCF.ConfigurationManager" Version="1.8.0" />
|
||||||
<Reference Include="NAudio.WinMM, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
<PackageReference Include="CoreWCF.Http" Version="1.8.0" />
|
||||||
<HintPath>..\packages\NAudio.WinMM.2.2.1\lib\netstandard2.0\NAudio.WinMM.dll</HintPath>
|
<PackageReference Include="CoreWCF.WebHttp" Version="1.8.0" />
|
||||||
</Reference>
|
<PackageReference Include="CoreWCF.NetTcp" Version="1.8.0" />
|
||||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
</ItemGroup>
|
||||||
<HintPath>..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
|
<ItemGroup>
|
||||||
</Reference>
|
<None Update="DialLayout.json">
|
||||||
<Reference Include="NLog, Version=6.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<HintPath>..\packages\NLog.6.0.5\lib\net46\NLog.dll</HintPath>
|
</None>
|
||||||
</Reference>
|
</ItemGroup>
|
||||||
<Reference Include="StreamDeckTools, Version=6.3.2.0, Culture=neutral, processorArchitecture=MSIL">
|
<ItemGroup>
|
||||||
<HintPath>..\packages\StreamDeck-Tools.6.3.2\lib\netstandard2.0\StreamDeckTools.dll</HintPath>
|
<Content Include="!!README!!.txt" />
|
||||||
</Reference>
|
<Content Include="Images\app_icon.png">
|
||||||
<Reference Include="System" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="System.Configuration" />
|
</Content>
|
||||||
<Reference Include="System.Core" />
|
<Content Include="Images\back.png">
|
||||||
<Reference Include="System.Drawing" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="System.Drawing.Common, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
</Content>
|
||||||
<HintPath>..\packages\System.Drawing.Common.9.0.10\lib\net462\System.Drawing.Common.dll</HintPath>
|
<Content Include="Images\category_icon.png">
|
||||||
</Reference>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="System.IO.Compression" />
|
</Content>
|
||||||
<Reference Include="System.Runtime.Serialization" />
|
<Content Include="Images\collection.png">
|
||||||
<Reference Include="System.Security.AccessControl, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<HintPath>..\packages\System.Security.AccessControl.4.7.0\lib\net461\System.Security.AccessControl.dll</HintPath>
|
</Content>
|
||||||
</Reference>
|
<Content Include="Images\collection_icon.png">
|
||||||
<Reference Include="System.Security.Principal.Windows, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<HintPath>..\packages\System.Security.Principal.Windows.4.7.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
|
</Content>
|
||||||
</Reference>
|
<Content Include="Images\page_nav.png">
|
||||||
<Reference Include="System.ServiceModel" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="System.Transactions" />
|
</Content>
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Content Include="Images\page_nav_icon.png">
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="Microsoft.CSharp" />
|
</Content>
|
||||||
<Reference Include="System.Data" />
|
<Content Include="Images\player_icon.png">
|
||||||
<Reference Include="System.Net.Http" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Reference Include="System.Xml" />
|
</Content>
|
||||||
</ItemGroup>
|
<Content Include="Images\record.png">
|
||||||
<ItemGroup>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Compile Include="BaseTest.cs" />
|
</Content>
|
||||||
<Compile Include="Client\ClipMetadata.cs" />
|
<Content Include="Images\player.png">
|
||||||
<Compile Include="Client\ClipTrimClient.cs" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Compile Include="Client\CollectionMetaData.cs" />
|
</Content>
|
||||||
<Compile Include="GlobalSettings.cs" />
|
<Content Include="Images\record_icon.png">
|
||||||
<Compile Include="Player.cs" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Compile Include="ProfileSwitcher.cs" />
|
</Content>
|
||||||
<Compile Include="Program.cs" />
|
<Content Include="manifest.json">
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<Compile Include="WavPlayer.cs" />
|
</Content>
|
||||||
</ItemGroup>
|
<Content Include="package.json" />
|
||||||
<ItemGroup>
|
<Content Include="PropertyInspector\profile_swticher.html">
|
||||||
<None Include="App.config" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<None Include="DialLayout.json">
|
</Content>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<Content Include="PropertyInspector\file_player.html">
|
||||||
</None>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<None Include="manifest.json">
|
</Content>
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
</ItemGroup>
|
||||||
</None>
|
|
||||||
<None Include="packages.config" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include="!!README!!.txt" />
|
|
||||||
<Content Include="Images\categoryIcon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\categoryIcon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\icon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\icon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginAction%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginAction.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginIcon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginIcon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="package.json" />
|
|
||||||
<Content Include="PropertyInspector\profile_swticher.html">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="PropertyInspector\file_player.html">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
|
||||||
<PropertyGroup>
|
|
||||||
<PreBuildEvent>npm run stop</PreBuildEvent>
|
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<PostBuildEvent>npm run start</PostBuildEvent>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
|
||||||
<StartArguments>--port 23654 --pluginUUID com.michal-courson.cliptrim --registerEvent restart --info {}</StartArguments>
|
<StartArguments>--port 23654 --pluginUUID com.michal-courson.cliptrim --registerEvent restart --info {}</StartArguments>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,40 +1,40 @@
|
|||||||
{
|
{
|
||||||
"id": "sampleDial",
|
"id": "sampleDial",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"key": "title",
|
"key": "title",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"rect": [ 16, 10, 136, 24 ],
|
"rect": [ 16, 10, 136, 24 ],
|
||||||
"font": {
|
"font": {
|
||||||
"size": 16,
|
"size": 16,
|
||||||
"weight": 600
|
"weight": 600
|
||||||
},
|
},
|
||||||
"alignment": "left"
|
"alignment": "left"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "icon",
|
"key": "icon",
|
||||||
"type": "pixmap",
|
"type": "pixmap",
|
||||||
"rect": [ 16, 40, 48, 48 ]
|
"rect": [ 16, 40, 48, 48 ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "value",
|
"key": "value",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"rect": [ 76, 40, 108, 32 ],
|
"rect": [ 76, 40, 108, 32 ],
|
||||||
"font": {
|
"font": {
|
||||||
"size": 24,
|
"size": 24,
|
||||||
"weight": 600
|
"weight": 600
|
||||||
},
|
},
|
||||||
"alignment": "right"
|
"alignment": "right"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "indicator",
|
"key": "indicator",
|
||||||
"type": "gbar",
|
"type": "gbar",
|
||||||
"rect": [ 76, 74, 108, 20 ],
|
"rect": [ 76, 74, 108, 20 ],
|
||||||
"value": 0,
|
"value": 0,
|
||||||
"subtype": 4,
|
"subtype": 4,
|
||||||
"bar_h": 12,
|
"bar_h": 12,
|
||||||
"border_w": 0,
|
"border_w": 0,
|
||||||
"bar_bg_c": "0:#427018,0.75:#705B1C,0.90:#702735,1:#702735"
|
"bar_bg_c": "0:#427018,0.75:#705B1C,0.90:#702735,1:#702735"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -1,109 +1,49 @@
|
|||||||
using BarRaider.SdTools;
|
using BarRaider.SdTools;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
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 BarRaider.SdTools.Wrappers;
|
using BarRaider.SdTools.Wrappers;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using NAudio.MediaFoundation;
|
|
||||||
|
namespace ClipTrimDotNet
|
||||||
namespace ClipTrimDotNet
|
{
|
||||||
{
|
public class GlobalSettings
|
||||||
public class FileEntry
|
{
|
||||||
{
|
public static GlobalSettings? _inst;
|
||||||
public FileEntry()
|
public static GlobalSettings Instance
|
||||||
{
|
{
|
||||||
Volume = 1.0;
|
get
|
||||||
Playtype = "Play/Overlap";
|
{
|
||||||
}
|
_inst ??= CreateDefaultSettings();
|
||||||
[JsonProperty(PropertyName = "Volume")]
|
return _inst;
|
||||||
public double Volume { get; set; }
|
}
|
||||||
[JsonProperty(PropertyName = "Playtype")]
|
set
|
||||||
public string Playtype { get; set; }
|
{
|
||||||
}
|
_inst = value;
|
||||||
public class CollectionEntry
|
}
|
||||||
{
|
}
|
||||||
public CollectionEntry()
|
public static GlobalSettings CreateDefaultSettings()
|
||||||
{
|
{
|
||||||
Files = new Dictionary<string, FileEntry>();
|
GlobalSettings instance = new GlobalSettings();
|
||||||
}
|
instance.ProfileName = null;
|
||||||
[JsonProperty(PropertyName = "Files")]
|
instance.PortNumber = 5010;
|
||||||
public Dictionary<string, FileEntry> Files { get; set; }
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GlobalSettings
|
|
||||||
{
|
[JsonProperty(PropertyName = "profileName")]
|
||||||
public static GlobalSettings? _inst;
|
public string? ProfileName { get; set; }
|
||||||
public static GlobalSettings Instance
|
|
||||||
{
|
[JsonProperty(PropertyName = "portNumber")]
|
||||||
get
|
public int? PortNumber { get; set; }
|
||||||
{
|
|
||||||
_inst ??= CreateDefaultSettings();
|
|
||||||
return _inst;
|
public void SetCurrentProfile(string profile)
|
||||||
}
|
{
|
||||||
set
|
ProfileName = profile;
|
||||||
{
|
}
|
||||||
_inst = value;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
public static GlobalSettings CreateDefaultSettings()
|
|
||||||
{
|
|
||||||
GlobalSettings instance = new GlobalSettings();
|
|
||||||
instance.BasePath = null;
|
|
||||||
instance.ProfileName = null;
|
|
||||||
instance.Collections = new Dictionary<string, CollectionEntry>();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[FilenameProperty]
|
|
||||||
[JsonProperty(PropertyName = "basePath")]
|
|
||||||
public string? BasePath { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "profileName")]
|
|
||||||
public string? ProfileName { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "outputDevice")]
|
|
||||||
public string? OutputDevice { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "collections")]
|
|
||||||
public Dictionary<string, CollectionEntry> Collections { get; set; }
|
|
||||||
|
|
||||||
public void SetCurrentProfile(string 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
BIN
stream_deck_plugin/ClipTrimDotNet/Images/app_icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/app_icon.psd
Normal file
BIN
stream_deck_plugin/ClipTrimDotNet/Images/back.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/back.psd
Normal file
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |