mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-09-26 13:30:10 +00:00
Compare commits
8 commits
34e19c187c
...
712204ca17
Author | SHA1 | Date | |
---|---|---|---|
|
712204ca17 | ||
|
396cd33990 | ||
|
1f1906f67d | ||
|
115356df2d | ||
|
50794e1af1 | ||
|
446b861a67 | ||
|
7b43323813 | ||
|
b676f7aeb4 |
13 changed files with 279 additions and 90 deletions
|
@ -2,6 +2,7 @@ import "phoenix_html"
|
|||
import {Socket} from "phoenix"
|
||||
import {LiveSocket} from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
import Sortable from "../vendor/sortable"
|
||||
|
||||
let nowSeconds = () => Math.round(Date.now() / 1000)
|
||||
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)
|
||||
|
@ -13,6 +14,17 @@ let execJS = (selector, attr) => {
|
|||
|
||||
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 = {
|
||||
mounted(){
|
||||
let hide = () => liveSocket.execJS(this.el, this.el.getAttribute("phx-click"))
|
||||
|
@ -318,4 +330,4 @@ liveSocket.connect()
|
|||
// >> liveSocket.enableDebug()
|
||||
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session
|
||||
// >> liveSocket.disableLatencySim()
|
||||
window.liveSocket = liveSocket
|
||||
window.liveSocket = liveSocket
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
/Users/chris/oss/phoenix_live_view/assets/js/phoenix_live_view
|
2
assets/vendor/sortable.js
vendored
Normal file
2
assets/vendor/sortable.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -162,14 +162,24 @@ defmodule LiveBeats.MediaLibrary do
|
|||
user = Accounts.get_user!(user.id)
|
||||
|
||||
multi =
|
||||
Enum.reduce(changesets, Ecto.Multi.new(), fn {ref, chset}, acc ->
|
||||
chset =
|
||||
Ecto.Multi.new()
|
||||
|> lock_playlist(user.id)
|
||||
|> Ecto.Multi.run(:starting_position, fn repo, _changes ->
|
||||
count = repo.one(from s in Song, where: s.user_id == ^user.id, select: count(s.id))
|
||||
{:ok, count - 1}
|
||||
end)
|
||||
|
||||
multi =
|
||||
changesets
|
||||
|> Enum.with_index()
|
||||
|> Enum.reduce(multi, fn {{ref, chset}, i}, acc ->
|
||||
Ecto.Multi.insert(acc, {:song, ref}, fn %{starting_position: pos_start} ->
|
||||
chset
|
||||
|> Song.put_user(user)
|
||||
|> Song.put_mp3_path()
|
||||
|> Song.put_server_ip()
|
||||
|
||||
Ecto.Multi.insert(acc, {:song, ref}, chset)
|
||||
|> Ecto.Changeset.put_change(:position, pos_start + i + 1)
|
||||
end)
|
||||
end)
|
||||
|> Ecto.Multi.run(:valid_songs_count, fn _repo, changes ->
|
||||
new_songs_count = changes |> Enum.filter(&match?({{:song, _ref}, _}, &1)) |> Enum.count()
|
||||
|
@ -241,7 +251,10 @@ defmodule LiveBeats.MediaLibrary do
|
|||
end
|
||||
|
||||
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_playlist(:asc)
|
||||
|> Repo.replica().all()
|
||||
end
|
||||
|
@ -320,7 +333,7 @@ defmodule LiveBeats.MediaLibrary do
|
|||
def get_next_song(%Song{} = song, %Profile{} = profile) do
|
||||
next =
|
||||
from(s in Song,
|
||||
where: s.user_id == ^song.user_id and s.id > ^song.id,
|
||||
where: s.user_id == ^song.user_id and s.position > ^song.position,
|
||||
limit: 1
|
||||
)
|
||||
|> order_by_playlist(:asc)
|
||||
|
@ -332,8 +345,7 @@ defmodule LiveBeats.MediaLibrary do
|
|||
def get_prev_song(%Song{} = song, %Profile{} = profile) do
|
||||
prev =
|
||||
from(s in Song,
|
||||
where: s.user_id == ^song.user_id and s.id < ^song.id,
|
||||
order_by: [desc: s.inserted_at, desc: s.id],
|
||||
where: s.user_id == ^song.user_id and s.position < ^song.position,
|
||||
limit: 1
|
||||
)
|
||||
|> order_by_playlist(:desc)
|
||||
|
@ -342,6 +354,49 @@ defmodule LiveBeats.MediaLibrary do
|
|||
prev || get_last_song(profile)
|
||||
end
|
||||
|
||||
def update_song_position(%Song{} = song, new_index) do
|
||||
old_index = song.position
|
||||
|
||||
multi =
|
||||
Ecto.Multi.new()
|
||||
|> lock_playlist(song.user_id)
|
||||
|> Ecto.Multi.run(: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, new_index}
|
||||
count -> {:ok, count - 1}
|
||||
end
|
||||
end)
|
||||
|> multi_update_all(:dec_positions, fn %{index: new_index} ->
|
||||
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)
|
||||
|> multi_update_all(:inc_positions, fn %{index: new_index} ->
|
||||
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)
|
||||
|> multi_update_all(:position, fn %{index: new_index} ->
|
||||
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
|
||||
song
|
||||
|> Song.changeset(attrs)
|
||||
|
@ -350,14 +405,27 @@ defmodule LiveBeats.MediaLibrary do
|
|||
|
||||
def delete_song(%Song{} = song) do
|
||||
delete_song_file(song)
|
||||
old_index = song.position
|
||||
|
||||
Ecto.Multi.new()
|
||||
|> lock_playlist(song.user_id)
|
||||
|> Ecto.Multi.delete(:delete, song)
|
||||
|> multi_update_all(:dec_positions, fn _ ->
|
||||
from(s in Song,
|
||||
where: s.user_id == ^song.user_id,
|
||||
where: s.position > ^old_index,
|
||||
update: [inc: [position: -1]]
|
||||
)
|
||||
end)
|
||||
|> update_user_songs_count(song.user_id, -1)
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, _} -> :ok
|
||||
other -> other
|
||||
{:ok, _} ->
|
||||
broadcast!(song.user_id, %Events.SongDeleted{song: song})
|
||||
:ok
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -448,7 +516,7 @@ defmodule LiveBeats.MediaLibrary do
|
|||
end
|
||||
|
||||
defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do
|
||||
from(s in query, order_by: [{^direction, s.inserted_at}, {^direction, s.id}])
|
||||
from(s in query, order_by: [{^direction, s.position}])
|
||||
end
|
||||
|
||||
defp broadcast!(user_id, msg) when is_integer(user_id) do
|
||||
|
@ -464,4 +532,12 @@ defmodule LiveBeats.MediaLibrary do
|
|||
{:error, :songs_limit_exceeded}
|
||||
end
|
||||
end
|
||||
|
||||
defp multi_update_all(multi, name, func, opts \\ []) do
|
||||
Ecto.Multi.update_all(multi, name, func, opts)
|
||||
end
|
||||
|
||||
defp lock_playlist(%Ecto.Multi{} = multi, user_id) do
|
||||
Repo.multi_transaction_lock(multi, :playlist, user_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,4 +14,12 @@ defmodule LiveBeats.MediaLibrary.Events do
|
|||
defmodule SongsImported do
|
||||
defstruct user_id: nil, songs: []
|
||||
end
|
||||
|
||||
defmodule NewPosition do
|
||||
defstruct song: nil
|
||||
end
|
||||
|
||||
defmodule SongDeleted do
|
||||
defstruct song: nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ defmodule LiveBeats.MediaLibrary.Song do
|
|||
field :date_recorded, :naive_datetime
|
||||
field :date_released, :naive_datetime
|
||||
field :duration, :integer
|
||||
field :status, Ecto.Enum, values: [stopped: 1, playing: 2, paused: 3]
|
||||
field :status, Ecto.Enum, values: [stopped: 1, playing: 2, paused: 3], default: :stopped
|
||||
field :title, :string
|
||||
field :attribution, :string
|
||||
field :mp3_url, :string
|
||||
|
@ -21,6 +21,7 @@ defmodule LiveBeats.MediaLibrary.Song do
|
|||
field :mp3_filename, :string
|
||||
field :mp3_filesize, :integer, default: 0
|
||||
field :server_ip, EctoNetwork.INET
|
||||
field :position, :integer, default: 0
|
||||
belongs_to :user, Accounts.User
|
||||
belongs_to :genre, LiveBeats.MediaLibrary.Genre
|
||||
|
||||
|
|
|
@ -4,6 +4,16 @@ defmodule LiveBeats.Repo do
|
|||
adapter: Ecto.Adapters.Postgres
|
||||
|
||||
def replica, do: LiveBeats.config([:replica])
|
||||
|
||||
@locks %{playlist: 1}
|
||||
|
||||
def multi_transaction_lock(multi, scope, id) when is_atom(scope) and is_integer(id) do
|
||||
scope_int = Map.fetch!(@locks, scope)
|
||||
|
||||
Ecto.Multi.run(multi, scope, fn repo, _changes ->
|
||||
repo.query("SELECT pg_advisory_xact_lock(#{scope_int}, #{id})")
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule LiveBeats.ReplicaRepo do
|
||||
|
|
|
@ -189,6 +189,7 @@ defmodule LiveBeatsWeb.CoreComponents do
|
|||
|
||||
slot :title
|
||||
slot :subtitle
|
||||
|
||||
slot :link do
|
||||
attr :navigate, :string
|
||||
attr :href, :string
|
||||
|
@ -550,21 +551,25 @@ defmodule LiveBeatsWeb.CoreComponents do
|
|||
"""
|
||||
end
|
||||
|
||||
attr :id, :any, default: nil
|
||||
attr :row_id, :any, default: false
|
||||
attr :row_click, :any, default: nil
|
||||
attr :row_remove, :any, default: nil
|
||||
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
|
||||
assigns =
|
||||
assigns
|
||||
|> assign_new(:row_id, fn -> false end)
|
||||
|> assign(:col, for(col <- assigns.col, col[:if] != false, do: col))
|
||||
|
||||
~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">
|
||||
<table class="min-w-full">
|
||||
<table id={@id} class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-t border-gray-200">
|
||||
<%= for col <- @col do %>
|
||||
|
@ -574,20 +579,32 @@ defmodule LiveBeatsWeb.CoreComponents do
|
|||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
<%= for {row, i} <- Enum.with_index(@rows) do %>
|
||||
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
|
||||
<%= for col <- @col do %>
|
||||
<td class={
|
||||
<tbody
|
||||
id={"#{@id}-body"}
|
||||
class="bg-white divide-y divide-gray-100"
|
||||
phx-update={@streamable && "stream"}
|
||||
phx-hook={@sortable_drop && "Sortable"}
|
||||
data-drop={@sortable_drop}
|
||||
>
|
||||
<tr
|
||||
:for={{row, i} <- Enum.with_index(@rows)}
|
||||
id={@row_id && @row_id.(row)}
|
||||
phx-remove={@row_remove && @row_remove.(row)}
|
||||
class="hover:bg-gray-50"
|
||||
>
|
||||
<td
|
||||
:for={col <- @col}
|
||||
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">
|
||||
<%= render_slot(col, row) %>
|
||||
</div>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
}
|
||||
>
|
||||
<div class="flex items-center space-x-3 lg:pl-2">
|
||||
<%= render_slot(col, row) %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -608,8 +625,6 @@ defmodule LiveBeatsWeb.CoreComponents do
|
|||
end
|
||||
|
||||
def live_table(assigns) do
|
||||
assigns = assign(assigns, :col, for(col <- assigns.col, col[:if] != false, do: col))
|
||||
|
||||
~H"""
|
||||
<div class="hidden mt-8 sm:block">
|
||||
<div class="align-middle inline-block min-w-full border-b border-gray-200">
|
||||
|
@ -633,7 +648,6 @@ defmodule LiveBeatsWeb.CoreComponents do
|
|||
index={i}
|
||||
active_id={@active_id}
|
||||
class="hover:bg-gray-50"
|
||||
,
|
||||
owns_profile?={@owns_profile?}
|
||||
/>
|
||||
<% end %>
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
|
||||
alias LiveBeats.{Accounts, MediaLibrary, MP3Stat}
|
||||
alias LiveBeatsWeb.{LayoutComponent, Presence}
|
||||
alias LiveBeatsWeb.ProfileLive.{SongRowComponent, UploadFormComponent}
|
||||
alias LiveBeatsWeb.ProfileLive.{UploadFormComponent}
|
||||
|
||||
@max_presences 20
|
||||
|
||||
|
@ -16,6 +16,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
<%= if @owns_profile? do %>
|
||||
(you)
|
||||
<% end %>
|
||||
– <%= ngettext("%{count} song", "%{count} songs", @songs_count) %>
|
||||
</div>
|
||||
<.link href={@profile.external_homepage_url} target="_blank" class="text-sm text-gray-600">
|
||||
<.icon name={:code} /> <span class=""><%= url_text(@profile.external_homepage_url) %></span>
|
||||
|
@ -59,8 +60,8 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
total_count={@presences_count}
|
||||
/>
|
||||
|
||||
<div id="dialogs" phx-update="append">
|
||||
<%= for song <- if(@owns_profile?, do: @songs, else: []), id = "delete-modal-#{song.id}" do %>
|
||||
<div id="dialogs" phx-update="stream">
|
||||
<%= for {_id, song} <- if(@owns_profile?, do: @streams.songs, else: []), id = "delete-modal-#{song.id}" do %>
|
||||
<.modal
|
||||
id={id}
|
||||
on_confirm={
|
||||
|
@ -78,24 +79,53 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<.live_table
|
||||
<.table
|
||||
id="songs"
|
||||
module={SongRowComponent}
|
||||
rows={@songs}
|
||||
row_id={fn song -> "song-#{song.id}" end}
|
||||
owns_profile?={@owns_profile?}
|
||||
rows={@streams.songs}
|
||||
row_id={fn {id, _song} -> id end}
|
||||
row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end}
|
||||
row_remove={fn {id, _song} -> hide("##{id}") end}
|
||||
streamable
|
||||
sortable_drop="row_dropped"
|
||||
>
|
||||
<:col :let={%{song: song}} label="Title"><%= song.title %></:col>
|
||||
<:col :let={%{song: song}} label="Artist"><%= song.artist %></:col>
|
||||
<:col
|
||||
:let={%{song: song}}
|
||||
:let={{_id, song}}
|
||||
label="Title"
|
||||
class!="px-6 py-3 text-sm font-medium text-gray-900 min-w-[200px] md:min-w-[20rem] cursor-pointer"
|
||||
>
|
||||
<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 %>
|
||||
</:col>
|
||||
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
|
||||
<:col
|
||||
:let={{_id, song}}
|
||||
label="Attribution"
|
||||
class="max-w-5xl break-words text-gray-600 font-light"
|
||||
>
|
||||
<%= song.attribution %>
|
||||
</:col>
|
||||
<:col :let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
|
||||
<:col :let={%{song: song}} label="" :if={@owns_profile?}>
|
||||
<:col :let={{_id, song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
|
||||
<:col :let={{_id, song}} :if={@owns_profile?} label="">
|
||||
<.link
|
||||
id={"delete-song-#{song.id}"}
|
||||
phx-click={show_modal("delete-modal-#{song.id}")}
|
||||
|
@ -104,7 +134,7 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
|
||||
</.link>
|
||||
</:col>
|
||||
</.live_table>
|
||||
</.table>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -123,22 +153,24 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
|
||||
active_song_id =
|
||||
if song = MediaLibrary.get_current_active_song(profile) do
|
||||
SongRowComponent.send_status(song.id, song.status)
|
||||
song.id
|
||||
end
|
||||
|
||||
songs = MediaLibrary.list_profile_songs(profile, 50)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(
|
||||
active_song_id: active_song_id,
|
||||
active_profile_id: current_user.active_profile_user_id,
|
||||
profile: profile,
|
||||
owns_profile?: MediaLibrary.owns_profile?(current_user, profile)
|
||||
owns_profile?: MediaLibrary.owns_profile?(current_user, profile),
|
||||
songs_count: Enum.count(songs)
|
||||
)
|
||||
|> list_songs()
|
||||
|> stream(:songs, songs)
|
||||
|> assign_presences()
|
||||
|
||||
{:ok, socket, temporary_assigns: [songs: [], presences: %{}]}
|
||||
{:ok, socket, temporary_assigns: [presences: %{}]}
|
||||
end
|
||||
|
||||
def handle_params(params, _url, socket) do
|
||||
|
@ -174,6 +206,18 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
{:noreply, socket}
|
||||
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
|
||||
{:noreply, assign_presence(socket, presence)}
|
||||
end
|
||||
|
@ -199,16 +243,32 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
|> push_patch(to: profile_path(update.profile))}
|
||||
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
|
||||
{:noreply, play_song(socket, song)}
|
||||
end
|
||||
|
||||
def handle_info({MediaLibrary, %MediaLibrary.Events.Pause{song: song}}, socket) do
|
||||
{:noreply, pause_song(socket, song.id)}
|
||||
{:noreply, pause_song(socket, song)}
|
||||
end
|
||||
|
||||
def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do
|
||||
{:noreply, update(socket, :songs, &(&1 ++ songs))}
|
||||
{:noreply,
|
||||
Enum.reduce(songs, socket, fn song, acc ->
|
||||
acc
|
||||
|> update(:songs_count, &(&1 + 1))
|
||||
|> stream_insert(:songs, song)
|
||||
end)}
|
||||
end
|
||||
|
||||
def handle_info({MediaLibrary, %MediaLibrary.Events.SongDeleted{song: song}}, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> update(:songs_count, &(&1 - 1))
|
||||
|> stream_delete(:songs, song)}
|
||||
end
|
||||
|
||||
def handle_info({MediaLibrary, {:ping, ping}}, socket) do
|
||||
|
@ -227,18 +287,20 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
def handle_info({Accounts, _}, socket), do: {:noreply, socket}
|
||||
|
||||
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
|
||||
assign(socket, :active_song_id, nil)
|
||||
else
|
||||
socket
|
||||
end
|
||||
socket =
|
||||
if socket.assigns.active_song_id == song_id do
|
||||
assign(socket, :active_song_id, nil)
|
||||
else
|
||||
socket
|
||||
end
|
||||
|
||||
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :stopped})
|
||||
end
|
||||
|
||||
defp pause_song(socket, song_id) do
|
||||
SongRowComponent.send_status(song_id, :paused)
|
||||
socket
|
||||
defp pause_song(socket, %MediaLibrary.Song{} = song) do
|
||||
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :paused})
|
||||
end
|
||||
|
||||
defp play_song(socket, %MediaLibrary.Song{} = song) do
|
||||
|
@ -246,19 +308,18 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
|
||||
cond do
|
||||
active_song_id == song.id ->
|
||||
SongRowComponent.send_status(song.id, :playing)
|
||||
socket
|
||||
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :playing})
|
||||
|
||||
active_song_id ->
|
||||
SongRowComponent.send_status(song.id, :playing)
|
||||
|
||||
socket
|
||||
|> stop_song(active_song_id)
|
||||
|> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
|
||||
|> assign(active_song_id: song.id)
|
||||
|
||||
true ->
|
||||
SongRowComponent.send_status(song.id, :playing)
|
||||
assign(socket, active_song_id: song.id)
|
||||
socket
|
||||
|> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
|
||||
|> assign(active_song_id: song.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -294,10 +355,6 @@ defmodule LiveBeatsWeb.ProfileLive do
|
|||
socket
|
||||
end
|
||||
|
||||
defp list_songs(socket) do
|
||||
assign(socket, songs: MediaLibrary.list_profile_songs(socket.assigns.profile, 50))
|
||||
end
|
||||
|
||||
defp assign_presences(socket) do
|
||||
socket = assign(socket, presences_count: 0, presences: %{}, presence_ids: %{})
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ defmodule LiveBeatsWeb.SettingsLive do
|
|||
|
||||
<div class="max-w-3xl px-4 mx-auto mt-6">
|
||||
<.form
|
||||
id="settings-form"
|
||||
:let={f}
|
||||
for={@changeset}
|
||||
phx-change="validate"
|
||||
|
@ -41,7 +42,7 @@ defmodule LiveBeatsWeb.SettingsLive do
|
|||
<.error
|
||||
field={:username}
|
||||
input_name="user[username]"
|
||||
errors={@changeset.errors}
|
||||
errors={f.errors}
|
||||
class="pt-2 pl-4 pr-4 ml-2 text-center"
|
||||
/>
|
||||
</div>
|
||||
|
@ -72,7 +73,7 @@ defmodule LiveBeatsWeb.SettingsLive do
|
|||
<.error
|
||||
field={:profile_tagline}
|
||||
input_name="user[profile_tagline]"
|
||||
errors={@changeset.errors}
|
||||
errors={f.errors}
|
||||
class="pt-2 pl-4 pr-4 ml-2 text-center"
|
||||
/>
|
||||
</div>
|
||||
|
@ -104,7 +105,7 @@ defmodule LiveBeatsWeb.SettingsLive do
|
|||
|
||||
def handle_event("validate", %{"user" => params}, socket) do
|
||||
changeset = Accounts.change_settings(socket.assigns.current_user, params)
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
{:noreply, assign(socket, changeset: Map.put(changeset, :action, :validate))}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"user" => params}, socket) do
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -32,14 +32,14 @@ defmodule LiveBeats.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.7.0-rc.0", override: true},
|
||||
{:phoenix_live_view, "~> 0.18.3"},
|
||||
{:phoenix, "~> 1.7.1"},
|
||||
{:phoenix_live_view, "~> 0.18.16"},
|
||||
{:phoenix_live_dashboard, "~> 0.7.2"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:ecto_network, "~> 1.3.0"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_html, "~> 3.3", override: true},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:floki, ">= 0.30.0", only: :test},
|
||||
{:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
|
||||
|
|
14
mix.lock
14
mix.lock
|
@ -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"},
|
||||
"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"},
|
||||
|
@ -22,14 +22,14 @@
|
|||
"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_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.1", "a029bde19d9c3b559e5c3d06c78b76e81396bedd456a6acedb42f9c7b2e535a9", [: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", "ea9d4a85c3592e37efa07d0dc013254fda445885facaefddcbf646375c116457"},
|
||||
"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.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
|
||||
"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_reload": {:hex, :phoenix_live_reload, "1.4.0", "4fe222c0be55fdc3f9c711e24955fc42a7cd9b7a2f5f406f2580a567c335a573", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "bebf0fc2d2113b61cb5968f585367234b7b4c21d963d691de7b4b2dc6cdaae6f"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.3", "2e3d009422addf8b15c3dccc65ce53baccbe26f7cfd21d264680b5867789a9c1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, 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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c8845177a866e017dcb7083365393c8f00ab061b8b6b2bda575891079dce81b2"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.16", "781c6a3ac49e0451ca403848b40807171caea400896fe8ed8e5ddd6106ad5580", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, 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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09e6ae2babe62f74bfcd1e3cac1a9b0e2c262557cc566300a843425c9cb6842a"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
|
@ -37,9 +37,9 @@
|
|||
"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"},
|
||||
"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_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.5.0", "f6bbce90226121d62a0715bca7c986c5e43de0ccc9475d79c55381d1796368cc", [:mix], [], "hexpm", "b51ac706df8a7a48a2c622ee02d09d68be8c40418698ffa909d73ae207eb5fb8"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.4.5", "30038a3715067f51a9580562c05a3a8d501126030336ffc6edb53bf57d6d2d26", [:mix], [{:bandit, "~> 0.6", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.4", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "1d9812dc7e703c205049426fd4fe0852a247a825f91b099e53dc96f68bafe4c8"},
|
||||
}
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue