defmodule LivebookWeb.OpenLive do use LivebookWeb, :live_view import LivebookWeb.SessionHelpers alias LivebookWeb.LayoutComponents alias Livebook.{Sessions, Notebook, FileSystem} on_mount LivebookWeb.SidebarHook @impl true def mount(params, _session, socket) do if connected?(socket) do Livebook.Sessions.subscribe() Livebook.NotebookManager.subscribe_recent_notebooks() end sessions = Sessions.list_sessions() |> Enum.filter(&(&1.mode == :default)) recent_notebooks = Livebook.NotebookManager.recent_notebooks() {:ok, assign(socket, tab: "file", initial_file: file_from_params(params), url: params["url"], sessions: sessions, recent_notebooks: recent_notebooks, page_title: "Open - Livebook" )} end @impl true def render(assigns) do ~H""" <:topbar_action> <.button color="blue" navigate={~p"/new"}> <.remix_icon icon="add-line" /> New notebook
<.link patch={~p"/open/storage"} class={["tab", @tab == "storage" && "active"]}> <.remix_icon icon="file-3-line" class="align-middle" /> From storage <.link patch={~p"/open/url"} class={["tab", @tab == "url" && "active"]}> <.remix_icon icon="download-cloud-2-line" class="align-middle" /> From URL <.link patch={~p"/open/source"} class={["tab", @tab == "source" && "active"]}> <.remix_icon icon="clipboard-line" class="align-middle" /> From source <.link patch={~p"/open/upload"} class={["tab", @tab == "upload" && "active"]}> <.remix_icon icon="file-upload-line" class="align-middle" /> File upload
<.live_component :if={@tab == "storage"} module={LivebookWeb.OpenLive.FileComponent} id="import-file" sessions={@sessions} initial_file={@initial_file} /> <.live_component :if={@tab == "url"} module={LivebookWeb.OpenLive.UrlComponent} id="import-url" url={@url} /> <.live_component :if={@tab == "source"} module={LivebookWeb.OpenLive.SourceComponent} id="import-source" /> <.live_component :if={@tab == "upload"} module={LivebookWeb.OpenLive.UploadComponent} id="import-upload" />

Recent notebooks

<%= if @recent_notebooks == [] do %> <.no_entries> Your most recently opened notebooks will appear here. <% else %> <.live_component module={LivebookWeb.NotebookCardsComponent} id="recent-notebook-list" notebook_infos={@recent_notebooks} sessions={@sessions} added_at_label="Opened" > <:card_icon :let={{_info, idx}}> <% end %>
Looking for unsaved notebooks? <.link class="font-semibold" navigate={~p"/open/storage?autosave=true"} phx-no-format >Browse them here.
""" end @impl true def handle_params(%{"tab" => tab}, _url, socket) when socket.assigns.live_action == :page do {:noreply, assign(socket, tab: tab)} end def handle_params(%{"url" => url}, _url, socket) when socket.assigns.live_action == :public_import do origin = Notebook.ContentLoader.url_to_location(url) origin |> Notebook.ContentLoader.fetch_content_from_location() |> case do {:ok, content} -> socket = import_source(socket, content, origin: origin) {:noreply, socket} {:error, _message} -> {:noreply, push_patch(socket, to: ~p"/open/url?url=#{url}")} end end def handle_params(%{"path" => path} = _params, _uri, socket) when socket.assigns.live_action == :public_open do expanded_path = Path.expand(path) if File.dir?(expanded_path) do {:noreply, push_patch(socket, to: ~p"/open/storage?path=#{path}")} else file = FileSystem.File.local(expanded_path) if file_running?(file, socket.assigns.sessions) do session_id = session_id_by_file(file, socket.assigns.sessions) {:noreply, push_navigate(socket, to: ~p"/sessions/#{session_id}")} else {:noreply, open_notebook(socket, file)} end end end def handle_params(_params, _url, socket), do: {:noreply, socket} @impl true def handle_event("hide_recent_notebook", %{"idx" => idx}, socket) do on_confirm = fn socket -> %{file: file} = Enum.fetch!(socket.assigns.recent_notebooks, idx) Livebook.NotebookManager.remove_recent_notebook(file) socket end {:noreply, confirm(socket, on_confirm, title: "Hide notebook", description: "The notebook will reappear here when you open it again.", confirm_text: "Hide", opt_out_id: "hide-notebook" )} end @impl true def handle_info({type, session} = event, socket) when type in [:session_created, :session_updated, :session_closed] and session.mode == :default do {:noreply, update(socket, :sessions, &update_session_list(&1, event))} end def handle_info({:fork, file}, socket) do {:noreply, fork_notebook(socket, file)} end def handle_info({:open, file}, socket) do {:noreply, open_notebook(socket, file)} end def handle_info({:import_source, source, session_opts}, socket) do socket = import_source(socket, source, session_opts) {:noreply, socket} end def handle_info({:recent_notebooks_updated, recent_notebooks}, socket) do {:noreply, assign(socket, recent_notebooks: recent_notebooks)} end def handle_info(_message, socket), do: {:noreply, socket} defp file_from_params(%{"autosave" => _} = _params) do Livebook.Settings.autosave_path() |> FileSystem.Utils.ensure_dir_path() |> FileSystem.File.local() end defp file_from_params(%{"path" => path} = _params) do path = Path.expand(path) cond do File.dir?(path) -> path |> FileSystem.Utils.ensure_dir_path() |> FileSystem.File.local() File.regular?(path) -> FileSystem.File.local(path) true -> Livebook.Config.local_file_system_home() end end defp file_from_params(_params), do: Livebook.Settings.default_dir() defp import_source(socket, source, session_opts) do {notebook, %{warnings: messages}} = Livebook.LiveMarkdown.notebook_from_livemd(source) socket = socket |> put_import_warnings(messages) |> put_flash( :info, "You have imported a notebook, no code has been executed so far. You should read and evaluate code as needed." ) session_opts = Keyword.merge(session_opts, notebook: notebook) create_session(socket, session_opts) end defp file_running?(file, sessions) do Enum.any?(sessions, &(&1.file == file)) end defp session_id_by_file(file, sessions) do session = Enum.find(sessions, &(&1.file == file)) session.id end end