metadata fixes, redux start. Lifecycle fixes for regions, etc
This commit is contained in:
@ -2,16 +2,12 @@
|
|||||||
"Test": [],
|
"Test": [],
|
||||||
"Uncategorized": [
|
"Uncategorized": [
|
||||||
{
|
{
|
||||||
|
"endTime": 20.085836909871187,
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260214_114317.wav",
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260214_114317.wav",
|
||||||
"name": "Clip 20260214_114317",
|
"name": "Farts",
|
||||||
"playbackType": "playStop",
|
"playbackType": "playStop",
|
||||||
|
"startTime": 17.124463519313306,
|
||||||
"volume": 0.8
|
"volume": 0.8
|
||||||
},
|
|
||||||
{
|
|
||||||
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260214_114712.wav",
|
|
||||||
"name": "Clip 20260214_114712",
|
|
||||||
"playbackType": "playStop",
|
|
||||||
"volume": 1.0
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
BIN
audio-service/src/__pycache__/main.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -97,15 +97,20 @@ class AudioRecorder:
|
|||||||
|
|
||||||
meta = MetaDataManager()
|
meta = MetaDataManager()
|
||||||
|
|
||||||
meta.add_clip_to_collection("Uncategorized",
|
clip_metadata = {
|
||||||
{
|
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"name": f"Clip {timestamp}",
|
"name": f"Clip {timestamp}",
|
||||||
"playbackType":"playStop",
|
"playbackType":"playStop",
|
||||||
"volume": 1.0,
|
"volume": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.add_clip_to_collection("Uncategorized",
|
||||||
|
{
|
||||||
|
clip_metadata
|
||||||
})
|
})
|
||||||
|
|
||||||
return filename
|
|
||||||
|
return clip_metadata
|
||||||
|
|
||||||
def set_buffer_duration(self, duration):
|
def set_buffer_duration(self, duration):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -8,14 +8,19 @@ from metadata_manager import MetaDataManager
|
|||||||
from settings import SettingsManager
|
from settings import SettingsManager
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
from routes.recording import recording_bp
|
from routes.recording import recording_bp
|
||||||
from routes.device import device_bp
|
from routes.device import device_bp
|
||||||
from routes.metadata import metadata_bp
|
from routes.metadata import metadata_bp
|
||||||
from routes.settings import settings_bp
|
from routes.settings import settings_bp
|
||||||
|
from flask_socketio import SocketIO
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
# socketio = SocketIO(app, cors_allowed_origins="*")
|
||||||
|
# CORS(socketio)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Create argument parser
|
# Create argument parser
|
||||||
@ -42,6 +47,7 @@ def main():
|
|||||||
app.register_blueprint(metadata_bp)
|
app.register_blueprint(metadata_bp)
|
||||||
app.register_blueprint(settings_bp)
|
app.register_blueprint(settings_bp)
|
||||||
app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
||||||
|
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
20
audio-service/src/metadata.json
Normal file
20
audio-service/src/metadata.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Uncategorized": [
|
||||||
|
{
|
||||||
|
"endTime": 12.489270386266055,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav",
|
||||||
|
"name": "Clip 20260214_133540",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 10.622317596566523,
|
||||||
|
"volume": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endTime": 6.824034334763957,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav",
|
||||||
|
"name": "Clip 20260214_133137",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 3.7982832618025753,
|
||||||
|
"volume": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@ -4,6 +4,12 @@ from metadata_manager import MetaDataManager
|
|||||||
|
|
||||||
metadata_bp = Blueprint('metadata', __name__)
|
metadata_bp = Blueprint('metadata', __name__)
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta', methods=['GET'])
|
||||||
|
def get_allmetadata():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
|
||||||
@metadata_bp.route('/meta/collections', methods=['GET'])
|
@metadata_bp.route('/meta/collections', methods=['GET'])
|
||||||
def get_collections():
|
def get_collections():
|
||||||
meta_manager = MetaDataManager()
|
meta_manager = MetaDataManager()
|
||||||
@ -16,7 +22,8 @@ def add_collection():
|
|||||||
collection_name = request.json.get('name')
|
collection_name = request.json.get('name')
|
||||||
try:
|
try:
|
||||||
meta_manager.create_collection(collection_name)
|
meta_manager.create_collection(collection_name)
|
||||||
return jsonify({'status': 'success', 'collection': collection_name})
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@ -30,14 +37,15 @@ def get_clips_in_collection(name):
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@metadata_bp.route('/meta/collection/clips/reorder', methods=['POST']):
|
@metadata_bp.route('/meta/collection/clips/reorder', methods=['POST'])
|
||||||
def reorder_clips_in_collection():
|
def reorder_clips_in_collection():
|
||||||
meta_manager = MetaDataManager()
|
meta_manager = MetaDataManager()
|
||||||
collection_name = request.json.get('name')
|
collection_name = request.json.get('name')
|
||||||
new_order = request.json.get('clips')
|
new_order = request.json.get('clips')
|
||||||
try:
|
try:
|
||||||
meta_manager.reorder_clips_in_collection(collection_name, new_order)
|
meta_manager.reorder_clips_in_collection(collection_name, new_order)
|
||||||
return jsonify({'status': 'success', 'clips': new_order})
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@ -48,8 +56,8 @@ def add_clip_to_collection():
|
|||||||
clip_metadata = request.json.get('clip')
|
clip_metadata = request.json.get('clip')
|
||||||
try:
|
try:
|
||||||
meta_manager.add_clip_to_collection(collection_name, clip_metadata)
|
meta_manager.add_clip_to_collection(collection_name, clip_metadata)
|
||||||
clips = meta_manager.get_clips_in_collection(collection_name)
|
collections = meta_manager.collections
|
||||||
return jsonify({'status': 'success', 'clips': clips})
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
@ -70,9 +78,10 @@ def edit_clip_in_collection():
|
|||||||
meta_manager = MetaDataManager()
|
meta_manager = MetaDataManager()
|
||||||
collection_name = request.json.get('name')
|
collection_name = request.json.get('name')
|
||||||
clip_metadata = request.json.get('clip')
|
clip_metadata = request.json.get('clip')
|
||||||
|
print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}")
|
||||||
try:
|
try:
|
||||||
meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
|
meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
|
||||||
clips = meta_manager.get_clips_in_collection(collection_name)
|
collections = meta_manager.collections
|
||||||
return jsonify({'status': 'success', 'clips': clips})
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from audio_recorder import AudioRecorder
|
from audio_recorder import AudioRecorder
|
||||||
|
import os
|
||||||
|
|
||||||
recording_bp = Blueprint('recording', __name__)
|
recording_bp = Blueprint('recording', __name__)
|
||||||
|
|
||||||
@ -32,3 +33,12 @@ def recording_status():
|
|||||||
print('HTTP: Checking recording status')
|
print('HTTP: Checking recording status')
|
||||||
status = 'recording' if recorder.is_recording() else 'stopped'
|
status = 'recording' if recorder.is_recording() else 'stopped'
|
||||||
return jsonify({'status': status})
|
return jsonify({'status': status})
|
||||||
|
|
||||||
|
@recording_bp.route('/record/delete', methods=['POST'])
|
||||||
|
def recording_delete():
|
||||||
|
filename = request.json.get('filename')
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
10
audio-service/src/settings.json
Normal file
10
audio-service/src/settings.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"input_device": {
|
||||||
|
"index": 0,
|
||||||
|
"name": "Microsoft Sound Mapper - Input",
|
||||||
|
"max_input_channels": 2,
|
||||||
|
"default_samplerate": 44100.0
|
||||||
|
},
|
||||||
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings",
|
||||||
|
"recording_length": 15
|
||||||
|
}
|
||||||
1085
electron-ui/package-lock.json
generated
1085
electron-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -107,8 +107,10 @@
|
|||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@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",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@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",
|
||||||
@ -117,7 +119,11 @@
|
|||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
|
"socketio": "^1.0.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"wavesurfer.js": "^7.12.1"
|
"wavesurfer.js": "^7.12.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
const AudioChannels = {
|
const AudioChannels = {
|
||||||
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
||||||
TRIM_FILE: 'audio:trimFile',
|
|
||||||
DELETE_FILE: 'audio:deleteFile',
|
|
||||||
GET_COLLECTION_METADATA: 'audio:getCollectionMetadata',
|
|
||||||
SET_COLLECTION_METADATA: 'audio:setCollectionMetadata',
|
|
||||||
ADD_COLLECTION: 'audio:addCollection',
|
|
||||||
REMOVE_COLLECTION: 'audio:removeCollection',
|
|
||||||
GET_COLLECTIONS: 'audio:getCollections',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default AudioChannels;
|
export default AudioChannels;
|
||||||
|
|||||||
@ -6,27 +6,3 @@ export interface LoadAudioBufferResult {
|
|||||||
buffer?: Buffer;
|
buffer?: Buffer;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PlaybackType {
|
|
||||||
PlayStop = 'playStop',
|
|
||||||
PlayOverlap = 'playOverlap',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClipMetadata {
|
|
||||||
name: string;
|
|
||||||
filePath: string;
|
|
||||||
volume: number;
|
|
||||||
startTime: number | undefined;
|
|
||||||
endTime: number | undefined;
|
|
||||||
playbackType: PlaybackType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CollectionMetadata {
|
|
||||||
name: string;
|
|
||||||
clips: ClipMetadata[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetadataFile {
|
|
||||||
uncategorizedClips: ClipMetadata[];
|
|
||||||
collections: CollectionMetadata[];
|
|
||||||
}
|
|
||||||
|
|||||||
62
electron-ui/src/redux/main.ts
Normal file
62
electron-ui/src/redux/main.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { createSlice, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { ClipMetadata, MetadataState } from './types';
|
||||||
|
|
||||||
|
const initialState: MetadataState = {
|
||||||
|
collections: {},
|
||||||
|
};
|
||||||
|
const metadataSlice = createSlice({
|
||||||
|
name: 'metadata',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setAllData(state, action) {
|
||||||
|
state.collections = action.payload.collections;
|
||||||
|
},
|
||||||
|
setCollections(state, action) {
|
||||||
|
const { collection, newMetadata } = action.payload;
|
||||||
|
state.collections[collection] = newMetadata;
|
||||||
|
},
|
||||||
|
editClip(state, action) {
|
||||||
|
const { collection, clip } = action.payload;
|
||||||
|
const clips = state.collections[collection];
|
||||||
|
// console.log('Editing clip in collection:', collection, clip);
|
||||||
|
if (clips) {
|
||||||
|
const index = clips.findIndex((c) => c.filename === clip.filename);
|
||||||
|
if (index !== -1) {
|
||||||
|
clips[index] = clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNewClips(state, action) {
|
||||||
|
const { collections } = action.payload;
|
||||||
|
Object.keys(collections).forEach((collection) => {
|
||||||
|
if (!state.collections[collection]) {
|
||||||
|
state.collections[collection] = [];
|
||||||
|
}
|
||||||
|
const existingFilenames = new Set(
|
||||||
|
state.collections[collection].map((clip) => clip.filename),
|
||||||
|
);
|
||||||
|
const newClips = collections[collection].filter(
|
||||||
|
(clip: ClipMetadata) => !existingFilenames.has(clip.filename),
|
||||||
|
);
|
||||||
|
state.collections[collection].push(...newClips);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: metadataSlice.reducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can still subscribe to the store
|
||||||
|
// store.subscribe(() => console.log(store.getState()));
|
||||||
|
|
||||||
|
// Get the type of our store variable
|
||||||
|
export type AppStore = typeof store;
|
||||||
|
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||||
|
export type RootState = ReturnType<AppStore['getState']>;
|
||||||
|
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||||
|
export type AppDispatch = AppStore['dispatch'];
|
||||||
|
|
||||||
|
export const { setCollections, addNewClips } = metadataSlice.actions;
|
||||||
|
export default metadataSlice.reducer;
|
||||||
17
electron-ui/src/redux/types.ts
Normal file
17
electron-ui/src/redux/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export enum PlaybackType {
|
||||||
|
PlayStop = 'playStop',
|
||||||
|
PlayOverlap = 'playOverlap',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipMetadata {
|
||||||
|
name: string;
|
||||||
|
filename: string;
|
||||||
|
volume: number;
|
||||||
|
startTime: number | undefined;
|
||||||
|
endTime: number | undefined;
|
||||||
|
playbackType: PlaybackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataState {
|
||||||
|
collections: Record<string, ClipMetadata[]>;
|
||||||
|
}
|
||||||
@ -1,19 +1,52 @@
|
|||||||
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
// import 'tailwindcss/tailwind.css';
|
// import 'tailwindcss/tailwind.css';
|
||||||
import icon from '../../assets/icon.svg';
|
import icon from '../../assets/icon.svg';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import ClipList from './components/ClipList';
|
import ClipList from './components/ClipList';
|
||||||
|
import { useAppDispatch } from './hooks';
|
||||||
|
import { store } from '../redux/main';
|
||||||
|
|
||||||
function MainPage() {
|
function MainPage() {
|
||||||
return <ClipList />;
|
const [collection, setCollection] = useState<string | null>('Uncategorized');
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMetadata = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5010/meta');
|
||||||
|
const data = await response.json();
|
||||||
|
// console.log('Fetched collections:', data.collections);
|
||||||
|
dispatch({ type: 'metadata/addNewClips', payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching metadata:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMetadata();
|
||||||
|
|
||||||
|
// 1. Set up the interval
|
||||||
|
const intervalId = setInterval(async () => {
|
||||||
|
fetchMetadata();
|
||||||
|
}, 5000); // 1000 milliseconds delay
|
||||||
|
|
||||||
|
// 2. Return a cleanup function to clear the interval when the component unmounts
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [dispatch]); //
|
||||||
|
return <ClipList collection={collection} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<MainPage />} />
|
<Route path="/" element={<MainPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,10 @@ 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 { useWavesurfer } from '@wavesurfer/react';
|
import { useWavesurfer } from '@wavesurfer/react';
|
||||||
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
import Regions 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';
|
||||||
@ -14,26 +18,53 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
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 } from '../../ipc/audio/types';
|
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||||
|
import { ClipMetadata } from '../../redux/types';
|
||||||
|
import { useAppSelector } from '../hooks';
|
||||||
|
|
||||||
export interface AudioTrimmerProps {
|
export interface AudioTrimmerProps {
|
||||||
metadata: ClipMetadata;
|
filename: string;
|
||||||
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
onSave?: (metadata: ClipMetadata) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioTrimmer({
|
export default function AudioTrimmer({
|
||||||
metadata,
|
filename,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: AudioTrimmerProps) {
|
}: AudioTrimmerProps) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
useSortable({ id: metadata.filePath });
|
useSortable({ id: filename });
|
||||||
|
|
||||||
|
const metadata = useAppSelector((state) => {
|
||||||
|
const clip = Object.values(state.collections)
|
||||||
|
.flat()
|
||||||
|
.find((c) => c.filename === filename);
|
||||||
|
return clip ?? ({ filename, name: 'Unknown Clip' } as ClipMetadata);
|
||||||
|
});
|
||||||
|
// Dialog state for editing name
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [nameInput, setNameInput] = useState<string>(metadata.name);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNameInput(metadata.name);
|
||||||
|
}, [metadata.name]);
|
||||||
|
|
||||||
|
const openEditDialog = () => setEditDialogOpen(true);
|
||||||
|
const closeEditDialog = () => setEditDialogOpen(false);
|
||||||
|
|
||||||
|
const handleDialogSave = () => {
|
||||||
|
if (nameInput.trim() && nameInput !== metadata.name) {
|
||||||
|
const updated = { ...metadata, name: nameInput.trim() };
|
||||||
|
if (onSave) onSave(updated);
|
||||||
|
}
|
||||||
|
closeEditDialog();
|
||||||
|
};
|
||||||
|
|
||||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const [clipStart, setClipStart] = useState<number>(0);
|
// const [clipStart, setClipStart] = useState<number | undefined>(undefined);
|
||||||
const [clipEnd, setClipEnd] = useState<number>(0);
|
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const plugins = useMemo(
|
const plugins = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@ -46,7 +77,7 @@ export default function AudioTrimmer({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const fileBaseName =
|
const fileBaseName =
|
||||||
metadata.filePath.split('\\').pop()?.split('/').pop() || 'Unknown';
|
metadata.filename.split('\\').pop()?.split('/').pop() || 'Unknown';
|
||||||
|
|
||||||
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||||
container: containerRef,
|
container: containerRef,
|
||||||
@ -58,56 +89,123 @@ export default function AudioTrimmer({
|
|||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add this ref to always have the latest metadata
|
||||||
|
const metadataRef = useRef(metadata);
|
||||||
|
useEffect(() => {
|
||||||
|
metadataRef.current = metadata;
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
const onRegionCreated = useCallback(
|
const onRegionCreated = useCallback(
|
||||||
(newRegion: any) => {
|
(newRegion: any) => {
|
||||||
if (wavesurfer === null) return;
|
if (wavesurfer === null) return;
|
||||||
const allRegions = plugins[0].getRegions();
|
|
||||||
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
|
let isNew = metadataRef.current.startTime === undefined;
|
||||||
|
|
||||||
allRegions.forEach((region) => {
|
allRegions.forEach((region) => {
|
||||||
if (region.id !== newRegion.id) {
|
if (region.id !== newRegion.id) {
|
||||||
|
if (
|
||||||
|
region.start === newRegion.start &&
|
||||||
|
region.end === newRegion.end
|
||||||
|
) {
|
||||||
|
newRegion.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
region.remove();
|
region.remove();
|
||||||
|
isNew = !(region.start === 0 && region.end === 0);
|
||||||
|
// console.log('Region replace:', newRegion, region);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
setClipStart(newRegion.start);
|
|
||||||
setClipEnd(newRegion.end);
|
if (isNew) {
|
||||||
|
console.log('Region created:', metadataRef.current);
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[plugins, wavesurfer],
|
[plugins, wavesurfer, onSave],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRegionUpdated = useCallback(
|
||||||
|
(newRegion: any) => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSave, wavesurfer],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ready, setting up regions plugin', wavesurfer);
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
if (metadata.startTime !== undefined && metadata.endTime !== undefined) {
|
|
||||||
setClipStart(metadata.startTime);
|
if (!isReady) return;
|
||||||
setClipEnd(metadata.endTime);
|
// console.log('ready, setting up regions plugin', plugin, isReady);
|
||||||
plugins[0].addRegion({
|
if (
|
||||||
start: metadata.startTime,
|
metadataRef.current.startTime !== undefined &&
|
||||||
end: metadata.endTime,
|
metadataRef.current.endTime !== undefined
|
||||||
|
) {
|
||||||
|
// setClipStart(metadata.startTime);
|
||||||
|
// setClipEnd(metadata.endTime);
|
||||||
|
// console.log('Adding region from metadata:', metadata);=
|
||||||
|
|
||||||
|
const allRegions = plugin.getRegions();
|
||||||
|
// console.log('Existing regions:', allRegions);
|
||||||
|
if (
|
||||||
|
allRegions.length === 0 ||
|
||||||
|
(allRegions.length === 1 &&
|
||||||
|
allRegions[0].start === 0 &&
|
||||||
|
allRegions[0].end === 0)
|
||||||
|
) {
|
||||||
|
// console.log('adding region from metadata:', metadataRef.current);
|
||||||
|
plugin.addRegion({
|
||||||
|
start: metadataRef.current.startTime,
|
||||||
|
end: metadataRef.current.endTime,
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
drag: false,
|
drag: false,
|
||||||
resize: true,
|
resize: true,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
setClipStart(0);
|
|
||||||
setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// setClipStart(0);
|
||||||
|
// setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
||||||
|
}
|
||||||
|
}, [isReady, plugins]);
|
||||||
|
|
||||||
plugins[0].enableDragSelection({
|
useEffect(() => {
|
||||||
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
|
plugin.unAll();
|
||||||
|
plugin.on('region-created', onRegionCreated);
|
||||||
|
plugin.on('region-updated', onRegionUpdated);
|
||||||
|
plugin.enableDragSelection({
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
});
|
});
|
||||||
plugins[0].on('region-created', onRegionCreated);
|
}, [onRegionCreated, onRegionUpdated, plugins]);
|
||||||
}, [isReady, plugins, wavesurfer, onRegionCreated, metadata]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let url: string | null = null;
|
let url: string | null = null;
|
||||||
async function fetchAudio() {
|
async function fetchAudio() {
|
||||||
// console.log('Loading audio buffer for file:', filePath);
|
// console.log('Loading audio buffer for file:', filename);
|
||||||
const buffer = await window.audio.loadAudioBuffer(metadata.filePath);
|
const buffer = await window.audio.loadAudioBuffer(metadata.filename);
|
||||||
console.log('Received buffer:', buffer.buffer);
|
// console.log('Received buffer:', buffer.buffer);
|
||||||
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]));
|
url = URL.createObjectURL(new Blob([audioData]));
|
||||||
console.log('Created blob URL:', url);
|
// console.log('Created blob URL:', url);
|
||||||
setBlobUrl(url);
|
setBlobUrl(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,14 +213,14 @@ export default function AudioTrimmer({
|
|||||||
return () => {
|
return () => {
|
||||||
if (url) URL.revokeObjectURL(url);
|
if (url) URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
}, [metadata.filePath]);
|
}, [metadata.filename]);
|
||||||
|
|
||||||
const onPlayPause = () => {
|
const onPlayPause = () => {
|
||||||
if (wavesurfer === null) return;
|
if (wavesurfer === null) return;
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
wavesurfer.pause();
|
wavesurfer.pause();
|
||||||
} else {
|
} else {
|
||||||
const allRegions = plugins[0].getRegions();
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
if (allRegions.length > 0) {
|
if (allRegions.length > 0) {
|
||||||
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
||||||
} else {
|
} else {
|
||||||
@ -149,7 +247,9 @@ 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"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...attributes}
|
{...attributes}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...listeners}
|
{...listeners}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -166,9 +266,63 @@ export default function AudioTrimmer({
|
|||||||
<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">
|
<div className="mb-5px flex flex-col">
|
||||||
<text className="font-bold text-lg">{metadata.name}</text>
|
<span
|
||||||
|
className="font-bold text-lg text-white mb-1 cursor-pointer"
|
||||||
|
onClick={openEditDialog}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openEditDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Click to edit name"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
style={{ outline: 'none' }}
|
||||||
|
>
|
||||||
|
{metadata.name}
|
||||||
|
</span>
|
||||||
<text className="text-sm text-neutral-500">{fileBaseName}</text>
|
<text className="text-sm text-neutral-500">{fileBaseName}</text>
|
||||||
</div>
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onClose={closeEditDialog}
|
||||||
|
slotProps={{
|
||||||
|
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Edit Clip Name</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<input
|
||||||
|
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"
|
||||||
|
type="text"
|
||||||
|
value={nameInput}
|
||||||
|
onChange={(e) => setNameInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleDialogSave();
|
||||||
|
}}
|
||||||
|
aria-label="Edit clip name"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -198,7 +352,8 @@ export default function AudioTrimmer({
|
|||||||
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
||||||
<div className="m-1 flex justify-start">
|
<div className="m-1 flex justify-start">
|
||||||
<text className="text-sm ">
|
<text className="text-sm ">
|
||||||
Clip: {formatTime(clipStart)} - {formatTime(clipEnd)}
|
Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
|
||||||
|
{formatTime(metadata.endTime ?? 0)}
|
||||||
</text>
|
</text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
import { use, useEffect, useState } from 'react';
|
|
||||||
import AudioTrimmer from './AudioTrimer';
|
|
||||||
import { ClipMetadata, PlaybackType } from '../../ipc/audio/types';
|
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
@ -12,99 +9,90 @@ import {
|
|||||||
arrayMove,
|
arrayMove,
|
||||||
SortableContext,
|
SortableContext,
|
||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
useSortable,
|
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
|
||||||
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
|
import AudioTrimmer from './AudioTrimer';
|
||||||
|
import { ClipMetadata } from '../../redux/types';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../hooks';
|
||||||
|
|
||||||
export interface ClipListProps {
|
export interface ClipListProps {
|
||||||
collection: string;
|
collection: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const testData: ClipMetadata[] = [
|
|
||||||
{
|
|
||||||
name: 'test 1',
|
|
||||||
filePath:
|
|
||||||
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav',
|
|
||||||
volume: 1,
|
|
||||||
startTime: undefined,
|
|
||||||
endTime: undefined,
|
|
||||||
playbackType: PlaybackType.PlayStop,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'test 2',
|
|
||||||
filePath:
|
|
||||||
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250105_131700.wav',
|
|
||||||
volume: 1,
|
|
||||||
startTime: undefined,
|
|
||||||
endTime: undefined,
|
|
||||||
playbackType: PlaybackType.PlayStop,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'test 3',
|
|
||||||
filePath:
|
|
||||||
'C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250117_194006.wav',
|
|
||||||
volume: 1,
|
|
||||||
startTime: undefined,
|
|
||||||
endTime: undefined,
|
|
||||||
playbackType: PlaybackType.PlayStop,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function SortableTrimmer({ trimmer, id }) {
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
||||||
useSortable({ id });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
marginBottom: '1rem',
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={setNodeRef} style={style}>
|
|
||||||
{/* Only this div is draggable */}
|
|
||||||
<div
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
style={{
|
|
||||||
cursor: 'grab',
|
|
||||||
padding: '4px',
|
|
||||||
background: '#6e44ba',
|
|
||||||
color: 'white',
|
|
||||||
borderRadius: '4px',
|
|
||||||
marginBottom: '8px',
|
|
||||||
width: 'fit-content',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Drag
|
|
||||||
</div>
|
|
||||||
{/* Wavesurfer and rest of AudioTrimmer are NOT draggable */}
|
|
||||||
<AudioTrimmer metadata={trimmer} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ClipList({ collection }: ClipListProps) {
|
export default function ClipList({ collection }: ClipListProps) {
|
||||||
const [metadata, setMetadata] = useState<ClipMetadata[]>(testData);
|
const metadata = useAppSelector(
|
||||||
useEffect(() => {
|
(state) => state.collections[collection] || [],
|
||||||
setMetadata(testData);
|
);
|
||||||
}, [collection]);
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleDragEnd(event) {
|
async function handleDragEnd(event) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
if (active.id !== over?.id) {
|
if (active.id !== over?.id) {
|
||||||
const oldIndex = metadata.findIndex(
|
const oldIndex = metadata.findIndex(
|
||||||
(item) => item.filePath === active.id,
|
(item) => item.filename === active.id,
|
||||||
);
|
);
|
||||||
const newIndex = metadata.findIndex((item) => item.filePath === over.id);
|
const newIndex = metadata.findIndex((item) => item.filename === over.id);
|
||||||
const newMetadata = arrayMove(metadata, oldIndex, newIndex);
|
const newMetadata = arrayMove(metadata, oldIndex, newIndex);
|
||||||
console.log('New order:', newMetadata);
|
console.log('New order:', newMetadata);
|
||||||
setMetadata(newMetadata);
|
dispatch({
|
||||||
|
type: 'metadata/setCollections',
|
||||||
|
payload: { collection, newMetadata },
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'http://localhost:5010/meta/collection/clips/reorder',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clips: newMetadata,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('handle reorder return:', data.collections);
|
||||||
|
dispatch({ type: 'metadata/setAllData', payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving new clip order:', error);
|
||||||
|
}
|
||||||
|
// setMetadata(newMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClipSave(meta: ClipMetadata) {
|
||||||
|
try {
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/editClip',
|
||||||
|
payload: { collection, clip: meta },
|
||||||
|
});
|
||||||
|
const response = await fetch(
|
||||||
|
'http://localhost:5010/meta/collection/clips/edit',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clip: meta,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
// console.log('handle clip save return:', data.collections);
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/editClip',
|
||||||
|
payload: { collection, clip: meta },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving clip metadata:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,11 +105,15 @@ export default function ClipList({ collection }: ClipListProps) {
|
|||||||
modifiers={[restrictToVerticalAxis]}
|
modifiers={[restrictToVerticalAxis]}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={metadata.map((item) => item.filePath)}
|
items={metadata.map((item) => item.filename)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
{metadata.map((trimmer) => (
|
{metadata.map((trimmer) => (
|
||||||
<AudioTrimmer key={trimmer.filePath} metadata={trimmer} />
|
<AudioTrimmer
|
||||||
|
key={trimmer.filename}
|
||||||
|
filename={trimmer.filename}
|
||||||
|
onSave={handleClipSave}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|||||||
6
electron-ui/src/renderer/hooks.ts
Normal file
6
electron-ui/src/renderer/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||||
|
import type { AppDispatch, RootState } from '../redux/main';
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
|
export const useAppStore = useStore.withTypes<RootState>();
|
||||||
Reference in New Issue
Block a user