This commit is contained in:
Chris McCord 2023-01-26 14:05:24 -05:00
parent 34e19c187c
commit b676f7aeb4
10 changed files with 181 additions and 57 deletions

View file

@ -1,6 +1,7 @@
import "phoenix_html" import "phoenix_html"
import {Socket} from "phoenix" import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view" // import {LiveSocket} from "phoenix_live_view"
import {LiveSocket} from "/Users/chris/oss/phoenix_live_view/assets/js/phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
let nowSeconds = () => Math.round(Date.now() / 1000) let nowSeconds = () => Math.round(Date.now() / 1000)
@ -13,6 +14,17 @@ let execJS = (selector, attr) => {
let Hooks = {} let Hooks = {}
Hooks.Sortable = {
mounted(){
let sorter = new Sortable(this.el, {
animation: 150,
onEnd: e => {
this.pushEvent(this.el.dataset["drop"], {id: e.item.id, old: e.oldIndex, new: e.newIndex})
}
})
}
}
Hooks.Flash = { Hooks.Flash = {
mounted(){ mounted(){
let hide = () => liveSocket.execJS(this.el, this.el.getAttribute("phx-click")) let hide = () => liveSocket.execJS(this.el, this.el.getAttribute("phx-click"))
@ -318,4 +330,4 @@ liveSocket.connect()
// >> liveSocket.enableDebug() // >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
// >> liveSocket.disableLatencySim() // >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket window.liveSocket = liveSocket

View file

@ -162,12 +162,15 @@ defmodule LiveBeats.MediaLibrary do
user = Accounts.get_user!(user.id) user = Accounts.get_user!(user.id)
multi = multi =
Enum.reduce(changesets, Ecto.Multi.new(), fn {ref, chset}, acc -> changesets
|> Enum.with_index()
|> Enum.reduce(Ecto.Multi.new(), fn {{ref, chset}, idx}, acc ->
chset = chset =
chset chset
|> Song.put_user(user) |> Song.put_user(user)
|> Song.put_mp3_path() |> Song.put_mp3_path()
|> Song.put_server_ip() |> Song.put_server_ip()
|> Ecto.Changeset.put_change(:position, idx)
Ecto.Multi.insert(acc, {:song, ref}, chset) Ecto.Multi.insert(acc, {:song, ref}, chset)
end) end)
@ -241,7 +244,7 @@ defmodule LiveBeats.MediaLibrary do
end end
def list_profile_songs(%Profile{} = profile, limit \\ 100) do def list_profile_songs(%Profile{} = profile, limit \\ 100) do
from(s in Song, where: s.user_id == ^profile.user_id, limit: ^limit) from(s in Song, where: s.user_id == ^profile.user_id, limit: ^limit, order_by: [asc: :position])
|> order_by_playlist(:asc) |> order_by_playlist(:asc)
|> Repo.replica().all() |> Repo.replica().all()
end end
@ -342,6 +345,48 @@ defmodule LiveBeats.MediaLibrary do
prev || get_last_song(profile) prev || get_last_song(profile)
end end
def update_song_position(%Song{} = song, new_index) do
old_index = song.position
multi =
Ecto.Multi.new()
|> Ecto.Multi.run(:valid_index, fn repo, _changes ->
case repo.one(from s in Song, where: s.user_id == ^song.user_id, select: count(s.id)) do
count when new_index < count -> {:ok, count}
_count -> {:error, :index_out_of_range}
end
end)
|> Ecto.Multi.update_all(:dec_positions, fn _ ->
from(s in Song,
where: s.user_id == ^song.user_id and s.id != ^song.id,
where: s.position > ^old_index and s.position <= ^new_index,
update: [inc: [position: -1]]
)
end, [])
|> Ecto.Multi.update_all(:inc_positions, fn _ ->
from(s in Song,
where: s.user_id == ^song.user_id and s.id != ^song.id,
where: s.position < ^old_index and s.position >= ^new_index,
update: [inc: [position: 1]]
)
end, [])
|> Ecto.Multi.update_all(:position, fn _ ->
from(s in Song,
where: s.id == ^song.id,
update: [set: [position: ^new_index]]
)
end, [])
case LiveBeats.Repo.transaction(multi) do
{:ok, _} ->
broadcast!(song.user_id, %Events.NewPosition{song: %Song{song | position: new_index}})
:ok
{:error, failed_op, _failed_val, _changes} ->
{:error, failed_op}
end
end
def update_song(%Song{} = song, attrs) do def update_song(%Song{} = song, attrs) do
song song
|> Song.changeset(attrs) |> Song.changeset(attrs)

View file

@ -14,4 +14,8 @@ defmodule LiveBeats.MediaLibrary.Events do
defmodule SongsImported do defmodule SongsImported do
defstruct user_id: nil, songs: [] defstruct user_id: nil, songs: []
end end
defmodule NewPosition do
defstruct song: nil
end
end end

View file

@ -21,6 +21,7 @@ defmodule LiveBeats.MediaLibrary.Song do
field :mp3_filename, :string field :mp3_filename, :string
field :mp3_filesize, :integer, default: 0 field :mp3_filesize, :integer, default: 0
field :server_ip, EctoNetwork.INET field :server_ip, EctoNetwork.INET
field :position, :integer, default: 0
belongs_to :user, Accounts.User belongs_to :user, Accounts.User
belongs_to :genre, LiveBeats.MediaLibrary.Genre belongs_to :genre, LiveBeats.MediaLibrary.Genre

View file

@ -189,6 +189,7 @@ defmodule LiveBeatsWeb.CoreComponents do
slot :title slot :title
slot :subtitle slot :subtitle
slot :link do slot :link do
attr :navigate, :string attr :navigate, :string
attr :href, :string attr :href, :string
@ -550,21 +551,24 @@ defmodule LiveBeatsWeb.CoreComponents do
""" """
end end
attr :id, :any, default: nil
attr :row_id, :any, default: false attr :row_id, :any, default: false
attr :row_click, :any, default: nil
attr :rows, :list, required: true attr :rows, :list, required: true
attr :streamable, :boolean, default: false
attr :sortable_drop, :string, default: nil
slot :col, required: true slot :col, required: true do
attr :label, :string
attr :class, :string
attr :class!, :string
end
def table(assigns) do def table(assigns) do
assigns =
assigns
|> assign_new(:row_id, fn -> false end)
|> assign(:col, for(col <- assigns.col, col[:if] != false, do: col))
~H""" ~H"""
<div class="hidden mt-8 sm:block"> <div class="mt-8 sm:block">
<div class="align-middle inline-block min-w-full border-b border-gray-200"> <div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="min-w-full"> <table id={@id} class="min-w-full">
<thead> <thead>
<tr class="border-t border-gray-200"> <tr class="border-t border-gray-200">
<%= for col <- @col do %> <%= for col <- @col do %>
@ -574,13 +578,23 @@ defmodule LiveBeatsWeb.CoreComponents do
<% end %> <% end %>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-100"> <tbody
id={"#{@id}-body"}
class="bg-white divide-y divide-gray-100"
phx-stream={@streamable}
phx-hook={@sortable_drop && "Sortable"}
data-drop={@sortable_drop}
>
<%= for {row, i} <- Enum.with_index(@rows) do %> <%= for {row, i} <- Enum.with_index(@rows) do %>
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50"> <tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
<%= for col <- @col do %> <%= for col <- @col do %>
<td class={ <td
"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"} #{col[:class]}" phx-click={@row_click && @row_click.(row)}
}> class={
col[:class!] ||
"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"} #{col[:class]}"
}
>
<div class="flex items-center space-x-3 lg:pl-2"> <div class="flex items-center space-x-3 lg:pl-2">
<%= render_slot(col, row) %> <%= render_slot(col, row) %>
</div> </div>
@ -608,8 +622,6 @@ defmodule LiveBeatsWeb.CoreComponents do
end end
def live_table(assigns) do def live_table(assigns) do
assigns = assign(assigns, :col, for(col <- assigns.col, col[:if] != false, do: col))
~H""" ~H"""
<div class="hidden mt-8 sm:block"> <div class="hidden mt-8 sm:block">
<div class="align-middle inline-block min-w-full border-b border-gray-200"> <div class="align-middle inline-block min-w-full border-b border-gray-200">
@ -633,7 +645,6 @@ defmodule LiveBeatsWeb.CoreComponents do
index={i} index={i}
active_id={@active_id} active_id={@active_id}
class="hover:bg-gray-50" class="hover:bg-gray-50"
,
owns_profile?={@owns_profile?} owns_profile?={@owns_profile?}
/> />
<% end %> <% end %>

View file

@ -9,6 +9,7 @@
<%= assigns[:page_title] || "LiveBeats" %> <%= assigns[:page_title] || "LiveBeats" %>
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script> <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</head> </head>
<body> <body>

View file

@ -3,7 +3,7 @@ defmodule LiveBeatsWeb.ProfileLive do
alias LiveBeats.{Accounts, MediaLibrary, MP3Stat} alias LiveBeats.{Accounts, MediaLibrary, MP3Stat}
alias LiveBeatsWeb.{LayoutComponent, Presence} alias LiveBeatsWeb.{LayoutComponent, Presence}
alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent} alias LiveBeatsWeb.ProfileLive.{UploadFormComponent}
@max_presences 20 @max_presences 20
@ -60,7 +60,7 @@ defmodule LiveBeatsWeb.ProfileLive do
/> />
<div id="dialogs" phx-update="append"> <div id="dialogs" phx-update="append">
<%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %> <%= for {_id, song} <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %>
<.modal <.modal
id={id} id={id}
on_confirm={ on_confirm={
@ -78,24 +78,48 @@ defmodule LiveBeatsWeb.ProfileLive do
<% end %> <% end %>
</div> </div>
<.live_table <.table
id="songs" id="songs"
module={SongRowComponent}
rows={@songs} rows={@songs}
row_id={fn song -> "song-#{song.id}" end} row_id={fn {id, _song} -> id end}
owns_profile?={@owns_profile?} row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end}
streamable
sortable_drop="row_dropped"
> >
<:col :let={%{song: song}} label="Title"><%= song.title %></:col> <:col :let={{_id, song}} label="Title" class!="px-6 py-3 text-sm font-medium text-gray-900 min-w-[20rem] cursor-pointer">
<:col :let={%{song: song}} label="Artist"><%= song.artist %></:col> <span :if={song.status == :playing} class="flex pt-1 relative mr-2 w-4">
<span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span>
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1" aria-label="Playing" role="button" />
</span>
<span :if={song.status == :paused} class="flex pt-1 relative mr-2 w-4">
<.icon
name={:volume_up}
class="h-5 w-5 -mt-1 -ml-1 text-gray-400"
aria-label="Paused"
role="button"
/>
</span>
<span :if={song.status == :stopped} class="flex relative w-6 -translate-x-1">
<.icon
:if={@owns_profile?}
name={:play}
class="h-5 w-5 text-gray-400"
aria-label="Play"
role="button"
/>
</span>
<%= song.title %> <%= @count %>
</:col>
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
<:col <:col
:let={%{song: song}} :let={{_id, song}}
label="Attribution" label="Attribution"
class="max-w-5xl break-words text-gray-600 font-light" class="max-w-5xl break-words text-gray-600 font-light"
> >
<%= song.attribution %> <%= song.attribution %>
</:col> </:col>
<:col :let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col> <:col :let={{_id, song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
<:col :let={%{song: song}} label="" :if={@owns_profile?}> <:col :let={{_id, song}} :if={@owns_profile?} label="">
<.link <.link
id={"delete-song-#{song.id}"} id={"delete-song-#{song.id}"}
phx-click={show_modal("delete-modal-#{song.id}")} phx-click={show_modal("delete-modal-#{song.id}")}
@ -104,7 +128,7 @@ defmodule LiveBeatsWeb.ProfileLive do
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete <.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
</.link> </.link>
</:col> </:col>
</.live_table> </.table>
""" """
end end
@ -123,22 +147,22 @@ defmodule LiveBeatsWeb.ProfileLive do
active_song_id = active_song_id =
if song = MediaLibrary.get_current_active_song(profile) do if song = MediaLibrary.get_current_active_song(profile) do
SongRowComponent.send_status(song.id, song.status)
song.id song.id
end end
socket = socket =
socket socket
|> assign( |> assign(
count: 0,
active_song_id: active_song_id, active_song_id: active_song_id,
active_profile_id: current_user.active_profile_user_id, active_profile_id: current_user.active_profile_user_id,
profile: profile, profile: profile,
owns_profile?: MediaLibrary.owns_profile?(current_user, profile) owns_profile?: MediaLibrary.owns_profile?(current_user, profile)
) )
|> list_songs() |> stream_songs()
|> assign_presences() |> assign_presences()
{:ok, socket, temporary_assigns: [songs: [], presences: %{}]} {:ok, socket, temporary_assigns: [presences: %{}]}
end end
def handle_params(params, _url, socket) do def handle_params(params, _url, socket) do
@ -174,6 +198,17 @@ defmodule LiveBeatsWeb.ProfileLive do
{:noreply, socket} {:noreply, socket}
end end
def handle_event("row_dropped", %{"id" => dom_id, "old" => old_idx, "new" => new_idx}, socket) do
"songs-" <> id = dom_id
song = MediaLibrary.get_song!(id)
if song.user_id == socket.assigns.current_user.id and song.position == old_idx do
:ok = MediaLibrary.update_song_position(song, new_idx)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info({LiveBeatsWeb.Presence, %{user_joined: presence}}, socket) do def handle_info({LiveBeatsWeb.Presence, %{user_joined: presence}}, socket) do
{:noreply, assign_presence(socket, presence)} {:noreply, assign_presence(socket, presence)}
end end
@ -199,12 +234,16 @@ defmodule LiveBeatsWeb.ProfileLive do
|> push_patch(to: profile_path(update.profile))} |> push_patch(to: profile_path(update.profile))}
end end
def handle_info({MediaLibrary, %MediaLibrary.Events.NewPosition{song: song}}, socket) do
{:noreply, stream_insert(socket, :songs, song, at: song.position)}
end
def handle_info({MediaLibrary, %MediaLibrary.Events.Play{song: song}}, socket) do def handle_info({MediaLibrary, %MediaLibrary.Events.Play{song: song}}, socket) do
{:noreply, play_song(socket, song)} {:noreply, play_song(socket, song)}
end end
def handle_info({MediaLibrary, %MediaLibrary.Events.Pause{song: song}}, socket) do def handle_info({MediaLibrary, %MediaLibrary.Events.Pause{song: song}}, socket) do
{:noreply, pause_song(socket, song.id)} {:noreply, pause_song(socket, song)}
end end
def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do
@ -227,18 +266,20 @@ defmodule LiveBeatsWeb.ProfileLive do
def handle_info({Accounts, _}, socket), do: {:noreply, socket} def handle_info({Accounts, _}, socket), do: {:noreply, socket}
defp stop_song(socket, song_id) do defp stop_song(socket, song_id) do
SongRowComponent.send_status(song_id, :stopped) song = MediaLibrary.get_song!(song_id)
if socket.assigns.active_song_id == song_id do socket =
assign(socket, :active_song_id, nil) if socket.assigns.active_song_id == song_id do
else assign(socket, :active_song_id, nil)
socket else
end socket
end
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :stopped})
end end
defp pause_song(socket, song_id) do defp pause_song(socket, %MediaLibrary.Song{} = song) do
SongRowComponent.send_status(song_id, :paused) stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :paused})
socket
end end
defp play_song(socket, %MediaLibrary.Song{} = song) do defp play_song(socket, %MediaLibrary.Song{} = song) do
@ -246,19 +287,18 @@ defmodule LiveBeatsWeb.ProfileLive do
cond do cond do
active_song_id == song.id -> active_song_id == song.id ->
SongRowComponent.send_status(song.id, :playing) stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :playing})
socket
active_song_id -> active_song_id ->
SongRowComponent.send_status(song.id, :playing)
socket socket
|> stop_song(active_song_id) |> stop_song(active_song_id)
|> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
|> assign(active_song_id: song.id) |> assign(active_song_id: song.id)
true -> true ->
SongRowComponent.send_status(song.id, :playing) socket
assign(socket, active_song_id: song.id) |> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
|> assign(active_song_id: song.id)
end end
end end
@ -294,8 +334,8 @@ defmodule LiveBeatsWeb.ProfileLive do
socket socket
end end
defp list_songs(socket) do defp stream_songs(socket) do
assign(socket, songs: MediaLibrary.list_profile_songs(socket.assigns.profile, 50)) stream(socket, :songs, MediaLibrary.list_profile_songs(socket.assigns.profile, 50))
end end
defp assign_presences(socket) do defp assign_presences(socket) do

View file

@ -32,8 +32,9 @@ defmodule LiveBeats.MixProject do
# Type `mix help deps` for examples and options. # Type `mix help deps` for examples and options.
defp deps do defp deps do
[ [
{:phoenix, "~> 1.7.0-rc.0", override: true}, {:phoenix, "~> 1.7.0-rc.2", override: true},
{:phoenix_live_view, "~> 0.18.3"}, # {:phoenix_live_view, "~> 0.18.3"},
{:phoenix_live_view, path: "~/oss/phoenix_live_view", override: true},
{:phoenix_live_dashboard, "~> 0.7.2"}, {:phoenix_live_dashboard, "~> 0.7.2"},
{:phoenix_ecto, "~> 4.4"}, {:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"}, {:ecto_sql, "~> 3.6"},

View file

@ -1,5 +1,5 @@
%{ %{
"castore": {:hex, :castore, "0.1.19", "a2c3e46d62b7f3aa2e6f88541c21d7400381e53704394462b9fd4f06f6d42bb6", [:mix], [], "hexpm", "e96e0161a5dc82ef441da24d5fa74aefc40d920f3a6645d15e1f9f3e66bb2109"}, "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
@ -22,7 +22,7 @@
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
"nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"phoenix": {:hex, :phoenix, "1.7.0-rc.0", "8e328572f496b5170e879da94baa57c5f878f354d50eac052c9a7c6d57c2cf54", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ed503f6c55184afc0a453e44e6ab2a09f014f59b7fdd682313fdc52ec2f82859"}, "phoenix": {:hex, :phoenix, "1.7.0-rc.2", "8faaff6f699aad2fe6a003c627da65d0864c868a4c10973ff90abfd7286c1f27", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "71abde2f67330c55b625dcc0e42bf76662dbadc7553c4f545c2f3759f40f7487"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"},
@ -37,7 +37,7 @@
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"swoosh": {:hex, :swoosh, "1.8.2", "af9a22ab2c0d20b266f61acca737fa11a121902de9466a39e91bacdce012101c", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d058ba750eafadb6c09a84a352c14c5d1eeeda6e84945fcc95785b7f3067b7db"}, "swoosh": {:hex, :swoosh, "1.8.2", "af9a22ab2c0d20b266f61acca737fa11a121902de9466a39e91bacdce012101c", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d058ba750eafadb6c09a84a352c14c5d1eeeda6e84945fcc95785b7f3067b7db"},
"tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"}, "tailwind": {:hex, :tailwind, "0.1.9", "25ba09d42f7bfabe170eb67683a76d6ec2061952dc9bd263a52a99ba3d24bd4d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "9213f87709c458aaec313bb5f2df2b4d2cedc2b630e4ae821bf3c54c47a56d0b"},
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
"websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"}, "websock": {:hex, :websock, "0.4.3", "184ac396bdcd3dfceb5b74c17d221af659dd559a95b1b92041ecb51c9b728093", [:mix], [], "hexpm", "5e4dd85f305f43fd3d3e25d70bec4a45228dfed60f0f3b072d8eddff335539cf"},

View file

@ -0,0 +1,9 @@
defmodule LiveBeats.Repo.Migrations.AddPositionToSongs do
use Ecto.Migration
def change do
alter table(:songs) do
add :position, :integer, null: false, default: 0
end
end
end