Skip to content

Commit 74251e2

Browse files
committed
Mac build Support with major bug fixes
1 parent 12db63d commit 74251e2

23 files changed

Lines changed: 5521 additions & 60 deletions

app/globals.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@
106106
}
107107
}
108108

109+
.hide-scroll::-webkit-scrollbar {
110+
display: none;
111+
}
112+
113+
.hide-scroll {
114+
-ms-overflow-style: none;
115+
scrollbar-width: none;
116+
}
109117
/* Custom scrollbar */
110118
::-webkit-scrollbar {
111119
width: 6px;

app/page.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ export default function Home() {
154154
}, [darkMode]);
155155

156156
const getDateString = (date) => {
157-
return date.toISOString().split("T")[0];
157+
const year = date.getFullYear();
158+
const month = String(date.getMonth() + 1).padStart(2, "0");
159+
const day = String(date.getDate()).padStart(2, "0");
160+
return `${year}-${month}-${day}`;
158161
};
159162

160163
const getCurrentDayTasks = () => {

components/task-list.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export function TaskList({
178178
};
179179

180180
return (
181-
<div className="h-full overflow-y-auto overflow-x-hidden p-4 px-0">
181+
<div className="h-full overflow-y-auto hide-scroll overflow-x-hidden p-4 px-0">
182182
<AnimatePresence>
183183
{/* Habits Section */}
184184
{habitTasks.length > 0 && (

components/timer-modal.js

Lines changed: 224 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ export function TimerModal({
4545
const [isOvertimeStarted, setIsOvertimeStarted] = useState(false);
4646
const [isMuted, setIsMuted] = useState(false);
4747

48-
// Audio refs
49-
const playingAudioRef = useRef(null);
50-
const breakAudioRef = useRef(null);
51-
const overtimeAudioRef = useRef(null);
48+
// Audio context and buffer refs for true seamless looping
49+
const audioContextRef = useRef(null);
50+
const audioBuffersRef = useRef({});
51+
const audioSourcesRef = useRef({});
52+
const gainNodesRef = useRef({});
53+
const isInitializedRef = useRef(false);
5254

5355
const presets = [
5456
{ value: "5", label: "5 min", seconds: 5 * 60 },
@@ -57,58 +59,202 @@ export function TimerModal({
5759
{ value: "50", label: "50 min", seconds: 50 * 60 },
5860
];
5961

60-
// Initialize audio elements
62+
// Initialize Web Audio API with aggressive looping
6163
useEffect(() => {
62-
playingAudioRef.current = new Audio("/music/playing.mp3");
63-
breakAudioRef.current = new Audio("/music/break.mp3");
64-
overtimeAudioRef.current = new Audio("/music/overtime.mp3");
65-
66-
// Set audio properties
67-
[
68-
playingAudioRef.current,
69-
breakAudioRef.current,
70-
overtimeAudioRef.current,
71-
].forEach((audio) => {
64+
const initAudio = async () => {
65+
try {
66+
// Create audio context
67+
const AudioContext = window.AudioContext || window.webkitAudioContext;
68+
audioContextRef.current = new AudioContext();
69+
70+
const loadAudioBuffer = async (url, key) => {
71+
try {
72+
const response = await fetch(url);
73+
const arrayBuffer = await response.arrayBuffer();
74+
const audioBuffer = await audioContextRef.current.decodeAudioData(
75+
arrayBuffer
76+
);
77+
78+
// Trim silence from beginning and end
79+
const trimmedBuffer = trimSilence(audioBuffer);
80+
audioBuffersRef.current[key] = trimmedBuffer;
81+
82+
// Create gain node for this audio
83+
gainNodesRef.current[key] = audioContextRef.current.createGain();
84+
gainNodesRef.current[key].gain.value = 0.2;
85+
gainNodesRef.current[key].connect(
86+
audioContextRef.current.destination
87+
);
88+
89+
console.log(
90+
`Audio ${key} loaded: ${trimmedBuffer.duration.toFixed(3)}s`
91+
);
92+
} catch (error) {
93+
console.error(`Failed to load audio ${key}:`, error);
94+
// Fallback to HTML5 audio
95+
createFallbackAudio(url, key);
96+
}
97+
};
98+
99+
// Load all audio files
100+
await Promise.all([
101+
loadAudioBuffer("/music/playing.mp3", "playing"),
102+
loadAudioBuffer("/music/break.mp3", "break"),
103+
loadAudioBuffer("/music/overtime.mp3", "overtime"),
104+
]);
105+
106+
isInitializedRef.current = true;
107+
console.log("Web Audio API initialized successfully");
108+
} catch (error) {
109+
console.error("Web Audio API initialization failed:", error);
110+
initFallbackAudio();
111+
}
112+
};
113+
114+
// Trim silence from audio buffer
115+
const trimSilence = (buffer) => {
116+
const threshold = 0; // Silence threshold
117+
const channelData = buffer.getChannelData(0);
118+
119+
// Find start of audio (first non-silent sample)
120+
let start = 0;
121+
for (let i = 0; i < channelData.length; i++) {
122+
if (Math.abs(channelData[i]) > threshold) {
123+
start = i;
124+
break;
125+
}
126+
}
127+
128+
// Find end of audio (last non-silent sample)
129+
let end = channelData.length - 1;
130+
for (let i = channelData.length - 1; i >= 0; i--) {
131+
if (Math.abs(channelData[i]) > threshold) {
132+
end = i;
133+
break;
134+
}
135+
}
136+
137+
// Create new buffer with trimmed audio
138+
const trimmedLength = end - start + 1;
139+
const trimmedBuffer = audioContextRef.current.createBuffer(
140+
buffer.numberOfChannels,
141+
trimmedLength,
142+
buffer.sampleRate
143+
);
144+
145+
for (let channel = 0; channel < buffer.numberOfChannels; channel++) {
146+
const originalData = buffer.getChannelData(channel);
147+
const trimmedData = trimmedBuffer.getChannelData(channel);
148+
for (let i = 0; i < trimmedLength; i++) {
149+
trimmedData[i] = originalData[start + i];
150+
}
151+
}
152+
153+
return trimmedBuffer;
154+
};
155+
156+
// Fallback to HTML5 audio if Web Audio API fails
157+
const createFallbackAudio = (url, key) => {
158+
const audio = new Audio(url);
72159
audio.loop = true;
73-
audio.volume = 0.3; // Set a reasonable default volume
74-
});
160+
audio.volume = 0.2;
161+
audio.preload = "auto";
162+
audioBuffersRef.current[key] = { audio, isFallback: true };
163+
};
164+
165+
const initFallbackAudio = () => {
166+
console.log("Using HTML5 Audio fallback");
167+
createFallbackAudio("/music/playing.mp3", "playing");
168+
createFallbackAudio("/music/break.mp3", "break");
169+
createFallbackAudio("/music/overtime.mp3", "overtime");
170+
isInitializedRef.current = true;
171+
};
172+
173+
initAudio();
75174

76-
// Cleanup function
77175
return () => {
78-
[
79-
playingAudioRef.current,
80-
breakAudioRef.current,
81-
overtimeAudioRef.current,
82-
].forEach((audio) => {
83-
if (audio) {
84-
audio.pause();
85-
audio.currentTime = 0;
86-
}
87-
});
176+
// Cleanup
177+
stopAllAudio();
178+
if (
179+
audioContextRef.current &&
180+
audioContextRef.current.state !== "closed"
181+
) {
182+
audioContextRef.current.close();
183+
}
88184
};
89185
}, []);
90186

91-
// Audio control function
92-
const playAudio = (audioRef, shouldPlay = !isMuted) => {
93-
if (shouldPlay && audioRef.current) {
94-
// Stop all other audio first
95-
stopAllAudio();
96-
audioRef.current.currentTime = 0;
97-
audioRef.current.play().catch((error) => {
98-
console.log("Audio playback failed:", error);
99-
});
187+
// Play audio with aggressive seamless looping
188+
const playAudio = (audioKey, shouldPlay = !isMuted) => {
189+
if (
190+
!shouldPlay ||
191+
!isInitializedRef.current ||
192+
!audioBuffersRef.current[audioKey]
193+
)
194+
return;
195+
196+
// Stop all other audio first
197+
stopAllAudio();
198+
199+
const buffer = audioBuffersRef.current[audioKey];
200+
201+
// Handle fallback audio
202+
if (buffer.isFallback) {
203+
buffer.audio.currentTime = 0;
204+
buffer.audio.play().catch(console.error);
205+
return;
206+
}
207+
208+
// Resume audio context if suspended
209+
if (audioContextRef.current.state === "suspended") {
210+
audioContextRef.current.resume();
100211
}
212+
213+
// Create and start buffer source with perfect looping
214+
const startSeamlessLoop = () => {
215+
if (audioSourcesRef.current[audioKey]) {
216+
audioSourcesRef.current[audioKey].stop();
217+
}
218+
219+
const source = audioContextRef.current.createBufferSource();
220+
source.buffer = buffer;
221+
source.loop = true;
222+
source.loopStart = 0;
223+
source.loopEnd = buffer.duration;
224+
225+
// Connect to gain node
226+
source.connect(gainNodesRef.current[audioKey]);
227+
228+
// Start immediately
229+
source.start(0);
230+
audioSourcesRef.current[audioKey] = source;
231+
232+
console.log(`Started seamless loop for ${audioKey}`);
233+
};
234+
235+
startSeamlessLoop();
101236
};
102237

103238
const stopAllAudio = () => {
104-
[
105-
playingAudioRef.current,
106-
breakAudioRef.current,
107-
overtimeAudioRef.current,
108-
].forEach((audio) => {
109-
if (audio) {
110-
audio.pause();
111-
audio.currentTime = 0;
239+
Object.keys(audioSourcesRef.current).forEach((key) => {
240+
const source = audioSourcesRef.current[key];
241+
if (source) {
242+
try {
243+
source.stop();
244+
source.disconnect();
245+
} catch (error) {
246+
// Source might already be stopped
247+
}
248+
delete audioSourcesRef.current[key];
249+
}
250+
});
251+
252+
// Stop fallback audio
253+
Object.keys(audioBuffersRef.current).forEach((key) => {
254+
const buffer = audioBuffersRef.current[key];
255+
if (buffer && buffer.isFallback) {
256+
buffer.audio.pause();
257+
buffer.audio.currentTime = 0;
112258
}
113259
});
114260
};
@@ -118,22 +264,46 @@ export function TimerModal({
118264
setIsMuted(newMutedState);
119265

120266
if (newMutedState) {
121-
// Muting - stop all audio
122267
stopAllAudio();
123268
} else {
124-
// Unmuting - resume appropriate audio based on current state
269+
// Resume appropriate audio
125270
if (isRunning) {
126271
if (timeLeft === 0 && isOvertimeStarted) {
127-
playAudio(overtimeAudioRef);
272+
playAudio("overtime");
128273
} else if (isBreak) {
129-
playAudio(breakAudioRef);
274+
playAudio("break");
130275
} else if (timeLeft > 0) {
131-
playAudio(playingAudioRef);
276+
playAudio("playing");
132277
}
133278
}
134279
}
135280
};
136281

282+
// Handle user interaction requirement for audio
283+
const handleFirstUserInteraction = () => {
284+
if (
285+
audioContextRef.current &&
286+
audioContextRef.current.state === "suspended"
287+
) {
288+
audioContextRef.current.resume();
289+
}
290+
};
291+
292+
// Add click listener for first user interaction
293+
useEffect(() => {
294+
document.addEventListener("click", handleFirstUserInteraction, {
295+
once: true,
296+
});
297+
document.addEventListener("touchstart", handleFirstUserInteraction, {
298+
once: true,
299+
});
300+
301+
return () => {
302+
document.removeEventListener("click", handleFirstUserInteraction);
303+
document.removeEventListener("touchstart", handleFirstUserInteraction);
304+
};
305+
}, []);
306+
137307
// Main countdown timer effect
138308
useEffect(() => {
139309
if (isRunning && timeLeft > 0) {
@@ -147,10 +317,9 @@ export function TimerModal({
147317
// Overtime counter effect
148318
useEffect(() => {
149319
if (isRunning && timeLeft === 0) {
150-
// Start overtime audio when entering overtime mode
151320
if (!isOvertimeStarted) {
152321
setIsOvertimeStarted(true);
153-
playAudio(overtimeAudioRef); // Uses default mute check
322+
playAudio("overtime");
154323
}
155324

156325
const overtimeInterval = setInterval(() => {
@@ -193,19 +362,18 @@ export function TimerModal({
193362
useEffect(() => {
194363
if (isRunning && !isMuted) {
195364
if (timeLeft === 0) {
196-
playAudio(overtimeAudioRef);
365+
playAudio("overtime");
197366
} else if (isBreak) {
198-
playAudio(breakAudioRef);
367+
playAudio("break");
199368
} else {
200-
playAudio(playingAudioRef);
369+
playAudio("playing");
201370
}
202371
} else {
203372
stopAllAudio();
204373
if (!isRunning) {
205374
setIsOvertimeStarted(false);
206375
}
207376
}
208-
// The dependency `timeLeft > 0` is a boolean that only changes when the timer hits zero.
209377
}, [isRunning, isBreak, timeLeft > 0, isMuted]);
210378

211379
const formatTime = (seconds) => {

0 commit comments

Comments
 (0)