Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrated from MUI & Emotion to NextUI w/ Tailwind #35

Merged
merged 23 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ Mixpoint solves a common problem that many "desktop" DJ's face - there are only

The goal of this app is to provide a focused user experience that delivers on the need to lay out a series of tracks, easily tweak the transition from one track to the next (a mix), and save the output as a finished set. Using ai, the software will recommend mixes based on mixes other people have created, and eventually mix tracks together for you.

Huge thanks to the MUI team for creating such a [kickass UI](https://mui.com/joy-ui/getting-started/overview/) freely available.
Huge thanks to the NextUI team for creating such a [kickass UI](https://nextui.org) freely available.

The project uses [Wavesurfer](https://wavesurfer-js.org/) for waveform analysis and navigation. Also thanks to John Heiner for the fun progress indicator.
The project uses [Wavesurfer](https://wavesurfer-js.org/) for waveform analysis and navigation.

Open source is more than a licensing strategy. It's a [movement](https://opensource.stackexchange.com/questions/9805/can-i-license-my-project-with-an-open-source-license-but-disallow-commercial-use). This work is made possible only by the labor of many open source contributers and their freely sourced efforts.

Expand All @@ -48,7 +48,7 @@ The app is built using `Remix` which uses esBuild (now Vite) for really fast hot

- Typescript
- Remix
- MUI (Joy)
- NextUI
- Teaful for App State
- Dexie (IndexedDb) for Persistent State
- WaveSurfer
Expand Down
34 changes: 21 additions & 13 deletions app/api/audioEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,23 @@ const audioEvents = {

// Adjust zoom based on previous mixPrefs
waveform.zoom(
beatResolution === 1 ? 80 : beatResolution === 0.5 ? 40 : 20
beatResolution === '1:1' ? 80 : beatResolution === '1:2' ? 40 : 20
)

// Remove analyzing overlay
setAppState.analyzing(prev => prev.filter(id => id !== trackId))
setAppState.analyzing(prev => {
prev.delete(trackId)
return prev
})

// Style scrollbar (this is a workaround for https://github.com/katspaugh/wavesurfer.js/issues/2933)
const style = document.createElement('style')
style.textContent = `::-webkit-scrollbar {
background: rgba(4, 146, 247, 0.5);
height: 18px;
}

::-webkit-scrollbar-corner, ::-webkit-scrollbar-track {
border-top: 1px solid var(--joy-palette-divider);
background-color: var(--joy-palette-background-surface);
border-top: 1px solid rgba(128,128,128,.3);
}

::-webkit-scrollbar-thumb {
Expand All @@ -82,6 +83,9 @@ const audioEvents = {
}`
waveform.getWrapper().appendChild(style)

// add classname value to waveform.getWrapper()
waveform.getWrapper().classList.add('wrapper')

// Update time
let [time] = getAudioState[trackId].time()
if (!time) {
Expand All @@ -93,7 +97,10 @@ const audioEvents = {
waveform.on('redraw', () => audioEvents.seek(trackId))
} else {
// Remove from stemsAnalyzing
setAppState.stemsAnalyzing(prev => prev.filter(id => id !== trackId))
setAppState.stemsAnalyzing(prev => {
prev.delete(trackId)
return prev
})
}

// Update BPM if adjusted
Expand Down Expand Up @@ -418,13 +425,13 @@ const audioEvents = {

// Adjust zoom
switch (beatResolution) {
case 0.25:
case '1:4':
waveform.zoom(20)
break
case 0.5:
case '1:2':
waveform.zoom(40)
break
case 1:
case '1:1':
waveform.zoom(80)
break
}
Expand Down Expand Up @@ -531,9 +538,7 @@ const audioEvents = {
stem: TrackPrefs['stemZoom'] | 'all'
) => {
// add track to analyzing state
setAppState.analyzing(prev =>
prev.includes(trackId) ? prev : [...prev, trackId]
)
setAppState.analyzing(prev => prev.add(trackId))

const [{ waveform }] = getAudioState[trackId]()
if (waveform) waveform.destroy()
Expand Down Expand Up @@ -575,7 +580,10 @@ const audioEvents = {
}

// Remove from stemsAnalyzing
setAppState.stemsAnalyzing(prev => prev.filter(id => id !== trackId))
setAppState.stemsAnalyzing(prev => {
prev.delete(trackId)
return prev
})
}
}

Expand Down
31 changes: 19 additions & 12 deletions app/api/audioHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ async function getTracksRecursively(
const trackArray: partialTrack[] = []

// Change sort order to lastModified so new tracks are visible at the top
await setPrefs('user', { sortColumn: 'lastModified', sortDirection: 'desc' })
await setPrefs('user', {
sortColumn: 'lastModified',
sortDirection: 'descending'
})

const filesToTracks = async (
fileOrDirectoryHandle: FileSystemFileHandle | FileSystemDirectoryHandle,
Expand Down Expand Up @@ -96,15 +99,16 @@ async function getTracksRecursively(
}
})
return []
} else return addTracksToDb()
}

return addTracksToDb()
}

const analyzeTracks = async (tracks: Track[]): Promise<Track[]> => {
// Set analyzing state now to avoid tracks appearing with 'analyze' button
setAppState.analyzing(analyzing => [
...analyzing,
...tracks.map(track => track.id)
])
setAppState.analyzing(
prev => new Set([...prev, ...tracks.map(track => track.id)])
)

// Return array of updated tracks
const updatedTracks: Track[] = []
Expand All @@ -115,9 +119,9 @@ const analyzeTracks = async (tracks: Track[]): Promise<Track[]> => {
// Change sort order to lastModified so new tracks are visible at the top
await setPrefs('user', {
sortColumn: 'lastModified',
sortDirection: 'desc'
sortDirection: 'descending'
})
setAppState.page(0)
setAppState.page(1)
sorted = true
}

Expand All @@ -142,7 +146,10 @@ const analyzeTracks = async (tracks: Track[]): Promise<Track[]> => {
updatedTracks.push(trackWithId)

// Remove from analyzing state
setAppState.analyzing(analyzing => analyzing.filter(id => id !== track.id))
setAppState.analyzing(prev => {
prev.delete(track.id)
return prev
})
}
return updatedTracks
}
Expand All @@ -160,7 +167,7 @@ const getAudioDetails = async (
}> => {
const file = await getPermission(track)
if (!file) {
setAppState.analyzing([])
setAppState.analyzing(new Set())
throw errorHandler('Permission to the file or folder was denied.')
}

Expand Down Expand Up @@ -223,10 +230,10 @@ const calcMarkers = async (trackId: Track['id']): Promise<void> => {

if (!duration) return errorHandler(`Please try adding ${name} again.`)

const { beatResolution = 1 } = await getTrackPrefs(trackId)
const { beatResolution = '1:4' } = await getTrackPrefs(trackId)

const beatInterval = 60 / (bpm || 1)
const skipLength = beatInterval * (1 / beatResolution)
const skipLength = beatInterval * Number(beatResolution.split(':')[1])

let startPoint = adjustedOffset || offset || 0

Expand Down
10 changes: 6 additions & 4 deletions app/api/db/__dbSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This file initializes Dexie (indexDB), defines the schema and creates tables

import Dexie from 'dexie'
import { Key } from 'react'

// eventually allow the user to change these
const STATE_ROW_LIMIT = 100
Expand Down Expand Up @@ -89,7 +90,7 @@ type MixSet = {
// disk, which cannot be done without interacting with the page first.
// Each file is a few megabytes, so the cache must be limited.
const STEMS = ['drums', 'bass', 'vocals', 'other'] as const
type Stem = typeof STEMS[number]
type Stem = (typeof STEMS)[number]

type TrackCache = {
id: Track['id']
Expand All @@ -103,7 +104,7 @@ type TrackCache = {
type TrackPrefs = Partial<{
id: Track['id']
adjustedBpm: Track['bpm']
beatResolution: 0.25 | 0.5 | 1
beatResolution: '1:1' | '1:2' | '1:4'
stemZoom: Stem
mixpointTime: number // seconds
}>
Expand All @@ -127,8 +128,9 @@ type SetPrefs = Partial<{

type UserPrefs = Partial<{
date: Date
sortDirection: 'asc' | 'desc'
sortColumn: keyof Track // track table order property
sortDirection: 'ascending' | 'descending'
sortColumn: Key
visibleColumns: Set<Key> // track table visible columns
stemsDirHandle: FileSystemDirectoryHandle // local folder on file system to store stems
}>

Expand Down
17 changes: 9 additions & 8 deletions app/api/db/appState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// This file handles application state that is not persisted through page refreshes, therefore not in IndexedDB. appState is different from Prefs in that it isn't persistent.

import { ButtonProps } from '@mui/joy'
import type { ButtonProps } from '@nextui-org/react'
import { Key } from 'react'
import createStore from 'teaful'
import type WaveSurfer from 'wavesurfer.js'
import { Stem, Track } from '~/api/db/dbHandlers'
Expand Down Expand Up @@ -56,27 +57,27 @@ const {
setStore: setAppState
} = createStore<{
search: string | number
selected: Track['id'][]
selected: Set<Key> // NextUI table uses string keys
rowsPerPage: number
page: number
showButton: number | null
openDrawer: boolean
processing: boolean
analyzing: Track['id'][]
stemsAnalyzing: Track['id'][]
analyzing: Set<Track['id']>
stemsAnalyzing: Set<Track['id']>
syncTimer: ReturnType<typeof requestAnimationFrame> | undefined
audioContext?: AudioContext
loggedIn: string // email address
}>({
search: '',
selected: [],
selected: new Set(),
rowsPerPage: 10,
page: 0,
page: 1,
showButton: null,
openDrawer: false,
processing: false,
analyzing: [],
stemsAnalyzing: [],
analyzing: new Set(),
stemsAnalyzing: new Set(),
syncTimer: undefined,
loggedIn: ''
})
Expand Down
2 changes: 1 addition & 1 deletion app/api/db/dbHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const putTracks = async (tracks: TrackIdOptional[]): Promise<Track[]> => {
return (await db.tracks.bulkGet(updatedTracks)) as Track[]
}

const removeTracks = async (ids: number[]): Promise<void> => {
const removeTracks = async (ids: Track['id'][]): Promise<void> => {
await db.tracks.bulkDelete(ids)

// Ensure we delete the file cache when a track is deleted
Expand Down
7 changes: 5 additions & 2 deletions app/api/fileHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const browseFile = async (trackSlot?: 0 | 1): Promise<void> => {

if (files?.length) {
const tracks = (await processTracks(files)) || []
addToMix(tracks[trackSlot || 0])
if (tracks.length === 1) addToMix(tracks[trackSlot || 0])
}
}

Expand Down Expand Up @@ -198,7 +198,10 @@ const validateTrackStemAccess = async (
const state = await checkAccess()
if (state === 'ready') {
// remove analyzing
setAppState.stemsAnalyzing(prev => prev.filter(id => id !== trackId))
setAppState.stemsAnalyzing(prev => {
prev.delete(trackId)
return prev
})
}

if (stemState !== state) setAudioState[trackId].stemState(state)
Expand Down
Loading
Loading