@freqhole/playlistz 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Show } from "solid-js";
|
|
2
|
+
import {
|
|
3
|
+
audioState,
|
|
4
|
+
togglePlayback,
|
|
5
|
+
playPlaylist,
|
|
6
|
+
} from "../services/audioService.js";
|
|
7
|
+
import { blobDownloadStates } from "../services/blobTransferService.js";
|
|
8
|
+
import type { Playlist } from "../types/playlist.js";
|
|
9
|
+
|
|
10
|
+
interface AudioPlayerProps {
|
|
11
|
+
playlist?: Playlist;
|
|
12
|
+
size?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function AudioPlayer(props: AudioPlayerProps) {
|
|
16
|
+
const handleClick = async () => {
|
|
17
|
+
try {
|
|
18
|
+
if (!props.playlist || props.playlist.songIds.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const currentPlaylist = audioState.currentPlaylist();
|
|
23
|
+
const isCurrentPlaylist =
|
|
24
|
+
currentPlaylist && currentPlaylist.id === props.playlist.id;
|
|
25
|
+
|
|
26
|
+
// if this playlist is current (playing or paused), toggle play/pause
|
|
27
|
+
if (isCurrentPlaylist) {
|
|
28
|
+
await togglePlayback();
|
|
29
|
+
}
|
|
30
|
+
// otherwise, play this playlist from the beginning
|
|
31
|
+
else {
|
|
32
|
+
await playPlaylist(props.playlist);
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error("error in audio player:", error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// check if current song is loading (mirrorz SongRow logic)
|
|
40
|
+
const isCurrentlyLoading = () => {
|
|
41
|
+
const currentSong = audioState.currentSong();
|
|
42
|
+
return (
|
|
43
|
+
currentSong?.id === audioState.selectedSongId() && audioState.isLoading()
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// check if the current song's blob is being fetched from a peer
|
|
48
|
+
const isFetchingBlob = () => {
|
|
49
|
+
const sha =
|
|
50
|
+
audioState.currentSong()?.sha ?? audioState.currentSong()?.sha256;
|
|
51
|
+
return sha ? blobDownloadStates().get(sha) === "downloading" : false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isSpinning = () => isCurrentlyLoading() || isFetchingBlob();
|
|
55
|
+
|
|
56
|
+
// check if this playlist is currently playing
|
|
57
|
+
const isThisPlaylistPlaying = () => {
|
|
58
|
+
if (!props.playlist) return false;
|
|
59
|
+
|
|
60
|
+
const currentPlaylist = audioState.currentPlaylist();
|
|
61
|
+
const isPlaying = audioState.isPlaying();
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
isPlaying && currentPlaylist && currentPlaylist.id === props.playlist.id
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
data-testid="btn-play-playlist"
|
|
71
|
+
onClick={handleClick}
|
|
72
|
+
aria-pressed={isThisPlaylistPlaying() ? "true" : "false"}
|
|
73
|
+
aria-busy={isSpinning()}
|
|
74
|
+
class={`inline-flex items-center justify-center ${props.size || "w-12 h-12"} disabled:bg-gray-600 disabled:cursor-not-allowed rounded-full text-white hover:text-magenta-200 transition-colors mx-2 ${isThisPlaylistPlaying() ? "bg-magenta-500" : "hover:bg-magenta-500"}`}
|
|
75
|
+
>
|
|
76
|
+
<Show
|
|
77
|
+
when={isSpinning()}
|
|
78
|
+
fallback={
|
|
79
|
+
<Show
|
|
80
|
+
when={isThisPlaylistPlaying()}
|
|
81
|
+
fallback={
|
|
82
|
+
<svg
|
|
83
|
+
class="w-10 h-10 ml-0.5"
|
|
84
|
+
fill="currentColor"
|
|
85
|
+
viewBox="0 0 20 20"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
fill-opacity="1.0"
|
|
89
|
+
fill-rule="evenodd"
|
|
90
|
+
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
|
91
|
+
clip-rule="evenodd"
|
|
92
|
+
/>
|
|
93
|
+
</svg>
|
|
94
|
+
}
|
|
95
|
+
>
|
|
96
|
+
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
|
|
97
|
+
<path
|
|
98
|
+
fill-rule="evenodd"
|
|
99
|
+
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
|
100
|
+
clip-rule="evenodd"
|
|
101
|
+
/>
|
|
102
|
+
</svg>
|
|
103
|
+
</Show>
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
<svg
|
|
107
|
+
class="w-6 h-6 animate-spin"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
viewBox="0 0 24 24"
|
|
111
|
+
>
|
|
112
|
+
<path
|
|
113
|
+
stroke-linecap="round"
|
|
114
|
+
stroke-linejoin="round"
|
|
115
|
+
stroke-width="2"
|
|
116
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
117
|
+
/>
|
|
118
|
+
</svg>
|
|
119
|
+
</Show>
|
|
120
|
+
</button>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
|
|
2
|
+
// hover-triggered marquee for long text in constrained rows.
|
|
3
|
+
// ported from tomb/client/spume/src/components/text/MarqueeText.tsx.
|
|
4
|
+
// scrolls on hover when content overflows; does nothing when it fits.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Accessor,
|
|
8
|
+
createEffect,
|
|
9
|
+
createMemo,
|
|
10
|
+
createSignal,
|
|
11
|
+
onMount,
|
|
12
|
+
} from "solid-js";
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
text: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
// external hover state - pass a signal accessor when the row manages hover
|
|
19
|
+
isHovering?: boolean | Accessor<boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let stylesInjected = false;
|
|
23
|
+
function injectStyles() {
|
|
24
|
+
if (stylesInjected) return;
|
|
25
|
+
stylesInjected = true;
|
|
26
|
+
const style = document.createElement("style");
|
|
27
|
+
style.textContent = `
|
|
28
|
+
@keyframes marquee-scroll {
|
|
29
|
+
0%, 5% { transform: translateX(0); }
|
|
30
|
+
45%, 55% { transform: translateX(var(--marquee-offset)); }
|
|
31
|
+
95%, 100%{ transform: translateX(0); }
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
document.head.appendChild(style);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MarqueeText(props: Props) {
|
|
38
|
+
const [overflows, setOverflows] = createSignal(false);
|
|
39
|
+
const [offset, setOffset] = createSignal(0);
|
|
40
|
+
const [internalHover, setInternalHover] = createSignal(false);
|
|
41
|
+
let containerRef: HTMLDivElement | undefined;
|
|
42
|
+
let textRef: HTMLSpanElement | undefined;
|
|
43
|
+
|
|
44
|
+
const isHovering = () => {
|
|
45
|
+
const ext = props.isHovering;
|
|
46
|
+
if (ext === undefined) return internalHover();
|
|
47
|
+
return typeof ext === "function" ? ext() : ext;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const checkOverflow = () => {
|
|
51
|
+
if (!containerRef || !textRef) return;
|
|
52
|
+
const cw = containerRef.offsetWidth;
|
|
53
|
+
const tw = textRef.scrollWidth;
|
|
54
|
+
const does = tw > cw;
|
|
55
|
+
setOverflows(does);
|
|
56
|
+
if (does) setOffset(cw - tw - 8);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
onMount(() => {
|
|
60
|
+
injectStyles();
|
|
61
|
+
requestAnimationFrame(checkOverflow);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
createEffect(() => {
|
|
65
|
+
props.text;
|
|
66
|
+
requestAnimationFrame(checkOverflow);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const duration = () => Math.max(2, 2 + Math.abs(offset()) * 0.02);
|
|
70
|
+
|
|
71
|
+
const animation = createMemo(() => {
|
|
72
|
+
if (!overflows() || !isHovering()) return "none";
|
|
73
|
+
return `marquee-scroll ${duration()}s ease-in-out infinite`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
ref={containerRef!}
|
|
79
|
+
class={`overflow-hidden ${props.class ?? ""}`}
|
|
80
|
+
title={props.title ?? props.text}
|
|
81
|
+
onMouseEnter={
|
|
82
|
+
props.isHovering === undefined
|
|
83
|
+
? () => setInternalHover(true)
|
|
84
|
+
: undefined
|
|
85
|
+
}
|
|
86
|
+
onMouseLeave={
|
|
87
|
+
props.isHovering === undefined
|
|
88
|
+
? () => setInternalHover(false)
|
|
89
|
+
: undefined
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
ref={textRef!}
|
|
94
|
+
class="block whitespace-nowrap"
|
|
95
|
+
style={{ "--marquee-offset": `${offset()}px`, animation: animation() }}
|
|
96
|
+
>
|
|
97
|
+
{props.text}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|