@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.
Files changed (180) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.changeset/nice-wolves-thank.md +5 -0
  3. package/.freqhole-versions.json +4 -0
  4. package/.github/copilot-instructions.md +201 -0
  5. package/.github/workflows/changesets.yml +50 -0
  6. package/.github/workflows/npm-publish.yml +124 -0
  7. package/.github/workflows/pr-checks.yml +103 -0
  8. package/README.md +30 -0
  9. package/build-component.js +141 -0
  10. package/build-zip-bundle-lib.js +44 -0
  11. package/config/playwright.config.ts +47 -0
  12. package/config/vite.config.ts +44 -0
  13. package/config/vitest.config.ts +39 -0
  14. package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
  15. package/dist/assets/index-CbOXzGiA.js +216 -0
  16. package/dist/assets/index-CbOXzGiA.js.map +1 -0
  17. package/dist/assets/index-TvJ6RFpy.css +1 -0
  18. package/dist/assets/midden-DceCrT_L.js +2 -0
  19. package/dist/assets/midden-DceCrT_L.js.map +1 -0
  20. package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
  21. package/dist/index.html +55 -0
  22. package/dist/sw.js +134 -0
  23. package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
  24. package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
  25. package/docs/E2E_TESTID_PLAN.md +234 -0
  26. package/docs/IROH_P2P_PLAN.md +302 -0
  27. package/docs/ROADMAP.md +695 -0
  28. package/docs/TODO.md +167 -0
  29. package/docs/bundle-embedding-plan.md +134 -0
  30. package/docs/standalone-refactor.md +184 -0
  31. package/e2e/all-playlists.spec.ts +220 -0
  32. package/e2e/audio-player.spec.ts +226 -0
  33. package/e2e/collaborative-features.spec.ts +229 -0
  34. package/e2e/contexts.ts +238 -0
  35. package/e2e/edit-panel.spec.ts +87 -0
  36. package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
  37. package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
  38. package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
  39. package/e2e/fixtures/chord-stack-3s.wav +0 -0
  40. package/e2e/fixtures/cover-anim.gif +0 -0
  41. package/e2e/fixtures/cover-blue.png +0 -0
  42. package/e2e/fixtures/cover-checkers.png +0 -0
  43. package/e2e/fixtures/cover-gradient.jpg +0 -0
  44. package/e2e/fixtures/cover-mono.gif +0 -0
  45. package/e2e/fixtures/cover-noise.png +0 -0
  46. package/e2e/fixtures/cover-plasma.webp +0 -0
  47. package/e2e/fixtures/cover-portrait.jpg +0 -0
  48. package/e2e/fixtures/cover-red.png +0 -0
  49. package/e2e/fixtures/cover-thumb.jpg +0 -0
  50. package/e2e/fixtures/cover-wide.webp +0 -0
  51. package/e2e/fixtures/generate.mjs +257 -0
  52. package/e2e/fixtures/long-drone-90s.mp3 +0 -0
  53. package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
  54. package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
  55. package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
  56. package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
  57. package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
  58. package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
  59. package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
  60. package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
  61. package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
  62. package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
  63. package/e2e/fixtures/tone-220hz-10s.wav +0 -0
  64. package/e2e/fixtures/tone-440hz-2s.wav +0 -0
  65. package/e2e/fixtures/tone-880hz-5s.wav +0 -0
  66. package/e2e/fixtures/tone-stereo-3s.wav +0 -0
  67. package/e2e/fixtures/user-provided/README.md +1 -0
  68. package/e2e/helpers/app.ts +143 -0
  69. package/e2e/helpers/hooks.ts +133 -0
  70. package/e2e/helpers/index.ts +12 -0
  71. package/e2e/helpers/media.ts +125 -0
  72. package/e2e/helpers.ts +10 -0
  73. package/e2e/p2p-collaboration.spec.ts +356 -0
  74. package/e2e/p2p-multi-peer.spec.ts +723 -0
  75. package/e2e/p2p-states.spec.ts +302 -0
  76. package/e2e/playback.spec.ts +56 -0
  77. package/e2e/playlist-crud.spec.ts +126 -0
  78. package/e2e/share-link-autoplay.spec.ts +129 -0
  79. package/e2e/sharing-access.spec.ts +205 -0
  80. package/e2e/sharing.spec.ts +195 -0
  81. package/e2e/song-cache-state.spec.ts +202 -0
  82. package/e2e/zip-bundle.spec.ts +855 -0
  83. package/eslint.config.js +114 -0
  84. package/index.html +54 -0
  85. package/package.json +119 -0
  86. package/public/sw.js +134 -0
  87. package/scripts/use-local.mjs +37 -0
  88. package/scripts/use-published.mjs +37 -0
  89. package/src/App.tsx +9 -0
  90. package/src/cli/check.ts +164 -0
  91. package/src/cli/generate.ts +184 -0
  92. package/src/cli/http.ts +88 -0
  93. package/src/cli/index.ts +65 -0
  94. package/src/cli/init.ts +18 -0
  95. package/src/components/AllPlaylistsPanel.tsx +713 -0
  96. package/src/components/AudioPlayer.tsx +122 -0
  97. package/src/components/MarqueeText.tsx +101 -0
  98. package/src/components/PlaylistCoverModal.tsx +519 -0
  99. package/src/components/PlaylistEditPanel.tsx +803 -0
  100. package/src/components/PlaylistSharePanel.tsx +1020 -0
  101. package/src/components/ShareLinkKnockPanel.tsx +144 -0
  102. package/src/components/SharePanel.tsx +584 -0
  103. package/src/components/SongEditModal.tsx +453 -0
  104. package/src/components/SongEditPanel.tsx +578 -0
  105. package/src/components/SongRow.tsx +689 -0
  106. package/src/components/index.tsx +494 -0
  107. package/src/components/playlist/index.tsx +1203 -0
  108. package/src/context/PlaylistzContext.tsx +74 -0
  109. package/src/dev-hooks.ts +35 -0
  110. package/src/hooks/createDocIndexQuery.ts +53 -0
  111. package/src/hooks/createDocStore.test.ts +303 -0
  112. package/src/hooks/createDocStore.ts +90 -0
  113. package/src/hooks/useDragAndDrop.test.ts +474 -0
  114. package/src/hooks/useDragAndDrop.ts +400 -0
  115. package/src/hooks/useImageModal.test.ts +174 -0
  116. package/src/hooks/useImageModal.ts +201 -0
  117. package/src/hooks/usePlaylistManager.test.ts +453 -0
  118. package/src/hooks/usePlaylistManager.ts +685 -0
  119. package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
  120. package/src/hooks/usePlaylistsQuery.ts +44 -0
  121. package/src/hooks/useSongState.test.ts +236 -0
  122. package/src/hooks/useSongState.ts +114 -0
  123. package/src/hooks/useUIState.ts +71 -0
  124. package/src/index.tsx +18 -0
  125. package/src/services/audioService.dev.ts +22 -0
  126. package/src/services/audioService.test.ts +1226 -0
  127. package/src/services/audioService.ts +1395 -0
  128. package/src/services/automergeRepo.test.ts +269 -0
  129. package/src/services/automergeRepo.ts +226 -0
  130. package/src/services/blobTransferService.dev.ts +119 -0
  131. package/src/services/blobTransferService.test.ts +441 -0
  132. package/src/services/blobTransferService.ts +702 -0
  133. package/src/services/docIndexService.test.ts +179 -0
  134. package/src/services/docIndexService.ts +118 -0
  135. package/src/services/fileProcessingService.test.ts +554 -0
  136. package/src/services/fileProcessingService.ts +239 -0
  137. package/src/services/imageService.test.ts +701 -0
  138. package/src/services/imageService.ts +365 -0
  139. package/src/services/indexedDBService.integration.test.ts +104 -0
  140. package/src/services/indexedDBService.test.ts +202 -0
  141. package/src/services/indexedDBService.ts +436 -0
  142. package/src/services/offlineService.test.ts +661 -0
  143. package/src/services/offlineService.ts +382 -0
  144. package/src/services/p2pService.test.ts +305 -0
  145. package/src/services/p2pService.ts +344 -0
  146. package/src/services/playlistDocService.test.ts +448 -0
  147. package/src/services/playlistDocService.ts +707 -0
  148. package/src/services/playlistDownloadService.test.ts +674 -0
  149. package/src/services/playlistDownloadService.ts +389 -0
  150. package/src/services/sharingService.test.ts +812 -0
  151. package/src/services/sharingService.ts +1073 -0
  152. package/src/services/sharingState.ts +161 -0
  153. package/src/services/songReactivity.test.ts +620 -0
  154. package/src/services/songReactivity.ts +145 -0
  155. package/src/services/standaloneService.test.ts +1025 -0
  156. package/src/services/standaloneService.ts +588 -0
  157. package/src/services/streamingAudioService.test.ts +275 -0
  158. package/src/services/streamingAudioService.ts +166 -0
  159. package/src/styles.css +428 -0
  160. package/src/test-setup.ts +547 -0
  161. package/src/types/global.d.ts +40 -0
  162. package/src/types/playlist.ts +99 -0
  163. package/src/utils/hashUtils.ts +41 -0
  164. package/src/utils/log.ts +97 -0
  165. package/src/utils/m3u.test.ts +172 -0
  166. package/src/utils/m3u.ts +136 -0
  167. package/src/utils/mockData.ts +166 -0
  168. package/src/utils/standaloneTemplates.test.ts +175 -0
  169. package/src/utils/standaloneTemplates.ts +83 -0
  170. package/src/utils/swTemplate.ts +84 -0
  171. package/src/utils/timeUtils.ts +166 -0
  172. package/src/utils/typeGuards.ts +171 -0
  173. package/src/web-component.tsx +98 -0
  174. package/src/zip-bundle/index.ts +7 -0
  175. package/src/zip-bundle/m3u.ts +45 -0
  176. package/src/zip-bundle/types.ts +50 -0
  177. package/src/zip-bundle/utils.ts +33 -0
  178. package/src/zip-bundle/zipBuilder.ts +309 -0
  179. package/tailwind.config.js +55 -0
  180. 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
+ }