15 Commits

103 changed files with 2202 additions and 1140 deletions

View File

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

View File

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

View File

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

View File

@ -10,8 +10,8 @@
"output_device": { "output_device": {
"channels": 2, "channels": 2,
"default_samplerate": 48000, "default_samplerate": 48000,
"index": 44, "index": 45,
"name": "VM to Headset (VB-Audio Voicemeeter VAIO)" "name": "VM to Discord (VB-Audio Voicemeeter VAIO)"
}, },
"http_port": 5010 "http_port": 5010
} }

View File

@ -121,7 +121,7 @@ class AudioIO:
} }
meta.add_clip_to_collection("Uncategorized", clip_metadata ) meta.add_clip_to_collection("Uncategorized", clip_metadata )
self.socket.emit('new_clip', clip_metadata)
return clip_metadata return clip_metadata

View File

@ -19,7 +19,7 @@ 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')
@socketio.on('connect') @socketio.on('connect')
def handle_connect(): def handle_connect():
@ -27,10 +27,18 @@ def handle_connect():
emit('full_data', MetaDataManager().collections) emit('full_data', MetaDataManager().collections)
@socketio.on('record_clip') @socketio.on('record_clip')
def record_clip(data): def record_clip():
io = AudioIO() io = AudioIO()
io.save_last_n_seconds(); io.save_last_n_seconds();
@socketio.on('play_clip')
def play_clip(data):
io = AudioIO()
print(f"Received play_clip event with data: {data}")
if data:
io.play_clip(data)
def main(): def main():
# Create argument parser # Create argument parser
parser = argparse.ArgumentParser(description='Audio Recording Service') parser = argparse.ArgumentParser(description='Audio Recording Service')
@ -65,8 +73,9 @@ def main():
app.register_blueprint(device_bp) app.register_blueprint(device_bp)
app.register_blueprint(metadata_bp) app.register_blueprint(metadata_bp)
app.register_blueprint(settings_bp) app.register_blueprint(settings_bp)
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) # 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=True) socketio.run(app, host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=False, allow_unsafe_werkzeug=True)

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
electron-ui/dll_err.txt Normal file

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -20,6 +20,20 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath> <OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Images\app_icon.png" />
<None Remove="Images\back.png" />
<None Remove="Images\category_icon.png" />
<None Remove="Images\collection.png" />
<None Remove="Images\collection_icon.png" />
<None Remove="Images\page_nav.png" />
<None Remove="Images\page_nav_icon.png" />
<None Remove="Images\player.png" />
<None Remove="Images\player_icon.png" />
<None Remove="Images\record.png" />
<None Remove="Images\record_icon.png" />
<None Remove="manifest.json" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
@ -54,34 +68,43 @@
<None Update="DialLayout.json"> <None Update="DialLayout.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="!!README!!.txt" /> <Content Include="!!README!!.txt" />
<Content Include="Images\categoryIcon%402x.png"> <Content Include="Images\app_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\categoryIcon.png"> <Content Include="Images\back.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\icon%402x.png"> <Content Include="Images\category_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\icon.png"> <Content Include="Images\collection.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginAction%402x.png"> <Content Include="Images\collection_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginAction.png"> <Content Include="Images\page_nav.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginIcon%402x.png"> <Content Include="Images\page_nav_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\pluginIcon.png"> <Content Include="Images\player_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\record.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\player.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\record_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="package.json" /> <Content Include="package.json" />

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

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

View File

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

View File

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

View File

@ -0,0 +1,140 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
public class DataSourceItem
{
public string label { get; set; } = "";
public string value { get; set; } = "";
}
[PluginActionId("com.michal-courson.cliptrim.profile-switcher")]
public class ProfileSwitcher : KeypadBase
{
private class PluginSettings
{
public static PluginSettings CreateDefaultSettings()
{
PluginSettings instance = new PluginSettings();
instance.ProfileName = null;
return instance;
}
[JsonProperty(PropertyName = "profileName")]
public string? ProfileName { get; set; }
}
#region Private Members
private PluginSettings settings;
#endregion
public ProfileSwitcher(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
if (payload.Settings == null || payload.Settings.Count == 0)
{
settings = PluginSettings.CreateDefaultSettings();
SaveSettings();
}
else
{
settings = payload.Settings.ToObject<PluginSettings>()!;
}
Logger.Instance.LogMessage(TracingLevel.INFO, $"ProfileSwitcher initialized with payload {JsonConvert.SerializeObject(payload)}");
GlobalSettingsManager.Instance.RequestGlobalSettings();
Connection.OnSendToPlugin += Connection_OnSendToPlugin;
SetTitle();
}
private async void SetTitle()
{
await Connection.SetTitleAsync(settings.ProfileName);
}
private async void Connection_OnSendToPlugin(object? sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.SendToPlugin> e)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles");
if (e.Event.Payload["event"] is null) return;
if (e.Event.Payload["event"]!.ToString() == "getProfiles")
{
var files = ClipTrimClient.Instance.GetCollectionNames();
var items = files.Select(x => new DataSourceItem { label = x, value = x});
var obj = new JObject
{
["event"] = "getProfiles",
["items"] = JArray.FromObject(items)
};
await Connection.SendToPropertyInspectorAsync(obj);
}
}
public override void Dispose()
{
}
public override async void KeyPressed(KeyPayload payload)
{
GlobalSettings.Instance.SetCurrentProfile(settings.ProfileName??"");
ClipTrimClient.Instance.PageMode = false;
PageNavigator.TickAll();
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
await Connection.SwitchProfileAsync("ClipTrim");
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void OnTick()
{
SetTitle();
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"ProfileSwitcher received settings {JsonConvert.SerializeObject(payload.Settings)}");
Tools.AutoPopulateSettings(settings, payload.Settings);
SaveSettings();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
ClipTrimClient.Instance.CheckPort();
//if (payload.Settings == null || payload.Settings.Count == 0)
//{
// var inst = GlobalSettings.Instance;
//}
//else
//{
// GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
//}
}
#region Private Methods
private Task SaveSettings()
{
return Connection.SetSettingsAsync(JObject.FromObject(settings));
}
#endregion
}
}

View File

@ -1,194 +0,0 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
using System.Text.Json.Serialization;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet
{
[PluginActionId("com.michal-courson.cliptrim.player")]
public class Player : KeypadBase
{
private ClipMetadata? metadata;
private KeyCoordinates coordinates;
private class PluginSettings
{
public static PluginSettings CreateDefaultSettings()
{
PluginSettings instance = new PluginSettings();
instance.Path = null;
instance.PlayType = "Play/Overlap";
instance.Index = 0;
instance.Volume = 1;
return instance;
}
[FilenameProperty]
[JsonPropertyName("path")]
public string? Path { get; set; }
[JsonPropertyName("index")]
public int? Index { get; set; }
[JsonPropertyName("playtype")]
public string PlayType { get; set; }
[JsonPropertyName("volume")]
public double Volume { get; set; }
}
#region Private Members
private PluginSettings settings;
#endregion
public Player(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
if (payload.Settings == null || payload.Settings.Count == 0)
{
this.settings = PluginSettings.CreateDefaultSettings();
SaveSettings();
}
else
{
this.settings = payload.Settings.ToObject<PluginSettings>();
}
this.coordinates = payload.Coordinates;
GlobalSettingsManager.Instance.RequestGlobalSettings();
CheckFile();
}
private void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
}
public int GetIndex()
{
return Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0);
}
private async void CheckFile()
{
//if (settings == null || GlobalSettings.Instance.ProfileName ==null) return;
metadata = ClipTrimClient.Instance.GetClipByPagedIndex(GetIndex());
await Connection.SetTitleAsync($"{metadata?.Name ?? ""}");
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Set title to {metadata?.Name ?? ""}");
return;
//var files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(GlobalSettings.Instance.BasePath), GlobalSettings.Instance.ProfileName), "*.wav", SearchOption.TopDirectoryOnly)
// .OrderBy(file => File.GetCreationTime(file))
// .ToArray();
//int? i = this.settings.Index;
//string new_path = "";
//if (i != null && i >= 0 && i < files.Length)
//{
// new_path = files[i ?? 0];
//}
//await Connection.SetTitleAsync(Path.GetFileNameWithoutExtension(new_path));
//if (new_path != settings.Path)
//{
// settings.Path = new_path;
// if(new_path != "")
// {
// FileEntry opts = GlobalSettings.Instance.GetFileOptionsInCurrentProfile(new_path);
// settings.Volume = opts.Volume;
// settings.PlayType = opts.Playtype;
// }
// await SaveSettings();
//}
}
private async void Connection_OnApplicationDidLaunch(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs<BarRaider.SdTools.Events.ApplicationDidLaunch> e)
{
}
private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e)
{
//titleParameters = e.Event?.Payload?.TitleParameters;
//userTitle = e.Event?.Payload?.Title;
}
public override void Dispose()
{
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
Connection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
}
public override void KeyPressed(KeyPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressedd");
Tools.AutoPopulateSettings(settings, payload.Settings);
// Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings));
ClipTrimClient.Instance.PlayClip(metadata);
//try
//{
// WavPlayer.Instance.Play(settings.Path, GlobalSettings.Instance.OutputDevice, settings.Volume, settings.PlayType == "Play/Overlap" ? WavPlayer.PlayMode.PlayOverlap : WavPlayer.PlayMode.PlayStop);
//}
//catch
//{
//}
}
public override void KeyReleased(KeyPayload payload) {
}
public override void OnTick() {
CheckFile();
}
public override async void ReceivedSettings(ReceivedSettingsPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "Player rec settings");
Tools.AutoPopulateSettings(settings, payload.Settings);
GlobalSettings.Instance.SetFileOptionsCurrentProfile(settings.Path, new FileEntry() { Playtype = settings.PlayType, Volume = settings.Volume });
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
//SaveSettings();
//CheckFile();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
if (payload.Settings == null || payload.Settings.Count == 0)
{
var inst = GlobalSettings.Instance;
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
}
else
{
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
}
//CheckFile();
}
#region Private Methods
private Task SaveSettings()
{
return Connection.SetSettingsAsync(JObject.FromObject(settings));
}
#endregion
}
}

View File

@ -1,165 +0,0 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet
{
public class DataSourceItem
{
public string label { get; set; }
public string value { get; set; }
}
[PluginActionId("com.michal-courson.cliptrim.profile-switcher")]
public class ProfileSwitcher : KeypadBase
{
private class PluginSettings
{
public static PluginSettings CreateDefaultSettings()
{
PluginSettings instance = new PluginSettings();
instance.ProfileName = null;
return instance;
}
[JsonProperty(PropertyName = "profileName")]
public string? ProfileName { get; set; }
}
#region Private Members
private PluginSettings settings;
#endregion
public ProfileSwitcher(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
if (payload.Settings == null || payload.Settings.Count == 0)
{
this.settings = PluginSettings.CreateDefaultSettings();
SaveSettings();
}
else
{
this.settings = payload.Settings.ToObject<PluginSettings>();
}
GlobalSettingsManager.Instance.RequestGlobalSettings();
Connection.OnSendToPlugin += Connection_OnSendToPlugin;
SetTitle();
}
private async void SetTitle()
{
await Connection.SetTitleAsync(settings.ProfileName + " B");
}
private async void Connection_OnSendToPlugin(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.SendToPlugin> e)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles");
if (e.Event.Payload["event"].ToString() == "getProfiles")
{
//string basePath = "C:\\Users\\mickl\\Music\\clips";
//var files = Directory.GetDirectories(basePath, "*", SearchOption.TopDirectoryOnly).Select(x => Path.GetFileNameWithoutExtension(x)).Where(x => x != "original");
var files = ClipTrimClient.Instance.GetCollectionNames();
var items = files.Select(x => new DataSourceItem { label = x, value = x});
var obj = new JObject();
obj["event"] = "getProfiles";
obj["items"] = JArray.FromObject(items);
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles return " + JsonConvert.SerializeObject(obj));
await Connection.SendToPropertyInspectorAsync(obj);
}
//if (e.Event.Payload["event"].ToString() == "getOutputDevices")
//{
// List<WaveOutCapabilities> devices = new List<WaveOutCapabilities>();
// for (int n = -1; n < WaveOut.DeviceCount; n++)
// {
// var caps = WaveOut.GetCapabilities(n);
// devices.Add(caps);
// }
// var items = devices.Select(x => new DataSourceItem { label = x.ProductName, value = x.ProductName });
// var obj = new JObject();
// obj["event"] = "getOutputDevices";
// obj["items"] = JArray.FromObject(items);
// //Logger.Instance.LogMessage(TracingLevel.INFO, "get devices return " + JsonConvert.SerializeObject(obj));
// await Connection.SendToPropertyInspectorAsync(obj);
//}
}
private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e)
{
}
public override void Dispose()
{
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
}
public override async void KeyPressed(KeyPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "KeyPressed");
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings));
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
ClipTrimClient.Instance.SetSelectedCollectionByName(settings.ProfileName);
GlobalSettings.Instance.SetCurrentProfile(settings.ProfileName);
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
await Connection.SwitchProfileAsync("ClipTrim");
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void OnTick()
{
SetTitle();
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
Tools.AutoPopulateSettings(settings, payload.Settings);
SaveSettings();
//tTitle();
//CheckFile();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
if (payload.Settings == null || payload.Settings.Count == 0)
{
var inst = GlobalSettings.Instance;
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
}
else
{
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
}
//CheckFile();
}
#region Private Methods
private Task SaveSettings()
{
return Connection.SetSettingsAsync(JObject.FromObject(settings));
}
#endregion
}
}

View File

@ -13,7 +13,7 @@ namespace ClipTrimDotNet
{ {
// Uncomment this line of code to allow for debugging // Uncomment this line of code to allow for debugging
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); } //while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
Client.ClipTrimClient.Instance.PortNumber = 5010; //Client.ClipTrimClient.Instance.PortNumber = 5010;
SDWrapper.Run(args); SDWrapper.Run(args);
} }
} }

View File

@ -11,7 +11,7 @@
<!-- <!--
Learn more about property inspector components at https://sdpi-components.dev/docs/components Learn more about property inspector components at https://sdpi-components.dev/docs/components
--> -->
<sdpi-item label="Index"> <!--<sdpi-item label="Index">
<sdpi-select setting="index" placeholder="file index"> <sdpi-select setting="index" placeholder="file index">
<option value="0">0</option> <option value="0">0</option>
<option value="1">1</option> <option value="1">1</option>
@ -39,7 +39,7 @@
</sdpi-item> </sdpi-item>
<sdpi-item label="Volume"> <sdpi-item label="Volume">
<sdpi-range setting="volume" min=".1" max="1" , step="0.05"></sdpi-range> <sdpi-range setting="volume" min=".1" max="1" , step="0.05"></sdpi-range>
</sdpi-item> </sdpi-item>-->
</body> </body>
</html> </html>

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