mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-11 23:44:23 +08:00
Add taskbar icon to desktop app (#1119)
This commit is contained in:
parent
88f3f8fc97
commit
e5092e4c99
8 changed files with 310 additions and 135 deletions
|
@ -111,7 +111,7 @@ defmodule AppBuilder.MacOS do
|
|||
|
||||
File.mkdir_p!("tmp")
|
||||
launcher_src_path = "tmp/Launcher.swift"
|
||||
File.write!(launcher_src_path, launcher(additional_paths))
|
||||
File.write!(launcher_src_path, launcher(release, additional_paths))
|
||||
launcher_path = Path.join([app_bundle_path, "Contents", "MacOS", app_name <> "Launcher"])
|
||||
File.mkdir_p!(Path.dirname(launcher_path))
|
||||
|
||||
|
@ -133,7 +133,7 @@ defmodule AppBuilder.MacOS do
|
|||
release
|
||||
end
|
||||
|
||||
defp launcher(additional_paths) do
|
||||
defp launcher(release, additional_paths) do
|
||||
additional_paths = Enum.map_join(additional_paths, ":", &"\\(resourcePath)#{&1}")
|
||||
|
||||
"""
|
||||
|
@ -149,7 +149,7 @@ defmodule AppBuilder.MacOS do
|
|||
let logFile = FileHandle(forUpdatingAtPath: logPath)
|
||||
logFile?.seekToEndOfFile()
|
||||
|
||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/mac_app", ofType: "")!
|
||||
let releaseScriptPath = Bundle.main.path(forResource: "rel/bin/#{release.name}", ofType: "")!
|
||||
|
||||
let resourcePath = Bundle.main.resourcePath ?? ""
|
||||
let additionalPaths = "#{additional_paths}"
|
||||
|
|
|
@ -4,6 +4,29 @@ defmodule AppBuilder.Windows do
|
|||
import AppBuilder.Utils
|
||||
require EEx
|
||||
|
||||
def __send_events__(server, input)
|
||||
|
||||
def __send_events__(server, "new_file") do
|
||||
send(server, {:new_file, ''})
|
||||
end
|
||||
|
||||
def __send_events__(server, "reopen_app") do
|
||||
send(server, {:reopen_app, ''})
|
||||
end
|
||||
|
||||
def __send_events__(server, "open_url:" <> url) do
|
||||
send(server, {:open_url, String.to_charlist(url)})
|
||||
end
|
||||
|
||||
def __send_events__(server, "open_file:" <> path) do
|
||||
path =
|
||||
path
|
||||
|> String.replace("\\", "/")
|
||||
|> String.to_charlist()
|
||||
|
||||
send(server, {:open_file, path})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a Windows installer.
|
||||
"""
|
||||
|
@ -36,6 +59,9 @@ defmodule AppBuilder.Windows do
|
|||
erl_exe = Path.join([tmp_dir, "rel", "erts-#{release.erts_version}", "bin", "erl.exe"])
|
||||
rcedit_path = ensure_rcedit()
|
||||
cmd!(rcedit_path, ["--set-icon", app_icon_path, erl_exe])
|
||||
manifest_path = Path.join(tmp_dir, "manifest.xml")
|
||||
File.write!(manifest_path, manifest())
|
||||
cmd!(rcedit_path, ["--application-manifest", manifest_path, erl_exe])
|
||||
|
||||
File.write!(Path.join(tmp_dir, "#{app_name}.vbs"), launcher_vbs(release, options))
|
||||
nsi_path = Path.join(tmp_dir, "#{app_name}.nsi")
|
||||
|
@ -51,6 +77,21 @@ defmodule AppBuilder.Windows do
|
|||
release
|
||||
end
|
||||
|
||||
# https://docs.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
||||
defp manifest do
|
||||
"""
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
"""
|
||||
end
|
||||
|
||||
code = """
|
||||
<%
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
|
@ -112,7 +153,7 @@ defmodule AppBuilder.Windows do
|
|||
<% end %>
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>" "" "<%= type.name %>"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\DefaultIcon" "" "$INSTDIR\\app_icon.ico"
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "%1"'
|
||||
WriteRegStr HKCR "<%= app_name %>.<%= type.name %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_file:%1"'
|
||||
<% end %>
|
||||
|
||||
<%= for url_scheme <- url_schemes do %>
|
||||
|
@ -122,7 +163,7 @@ defmodule AppBuilder.Windows do
|
|||
WriteRegStr HKCR "<%= url_scheme %>" "URL Protocol" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open" "" ""
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "%1"'
|
||||
WriteRegStr HKCR "<%= url_scheme %>\\shell\\open\\command" "" '$WINDIR\\system32\\wscript.exe "$INSTDIR\\<%= app_name %>.vbs" "open_url:%1"'
|
||||
<% end %>
|
||||
SectionEnd
|
||||
|
||||
|
@ -143,16 +184,10 @@ defmodule AppBuilder.Windows do
|
|||
<%
|
||||
app_name = Keyword.fetch!(options, :name)
|
||||
module = Keyword.fetch!(options, :module)
|
||||
%>
|
||||
' This vbs script avoids a flashing cmd window when launching the release bat file
|
||||
%>' This vbs script avoids a flashing cmd window when launching the release bat file
|
||||
|
||||
path = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName)) & "rel\bin\<%= release.name %>.bat"
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
url = WScript.Arguments(0)
|
||||
Else
|
||||
url = ""
|
||||
End If
|
||||
root = Left(Wscript.ScriptFullName, Len(Wscript.ScriptFullName) - Len(Wscript.ScriptName))
|
||||
script = root & "rel\bin\<%= release.name %>.bat"
|
||||
|
||||
Set shell = CreateObject("WScript.Shell")
|
||||
|
||||
|
@ -167,16 +202,35 @@ defmodule AppBuilder.Windows do
|
|||
Set env = shell.Environment("Process")
|
||||
env("PATH") = ".\rel\vendor\elixir\bin;.\rel\erts-<%= release.erts_version %>\bin;" & env("PATH")
|
||||
|
||||
' > bin/release rpc "mod.windows_connected(url)"
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
input = WScript.Arguments(0)
|
||||
Else
|
||||
input = "reopen_app"
|
||||
End If
|
||||
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' We send the URL through IO, as opposed through the rpc expression, to avoid RCE.
|
||||
cmd = "echo """ & url & """ | """ & path & """ rpc <%= inspect(module) %>.windows_connected(IO.read(:line))"
|
||||
' $ bin/release rpc 'AppBuilder.Windows.__send_events__(MyApp, input)'
|
||||
'
|
||||
' We send the input through IO, as opposed using the rpc expression, to avoid RCE.
|
||||
cmd = "echo " & input & " | \""" & script & \""" rpc ""AppBuilder.Windows.__send_events__(<%= inspect(module) %>, String.trim(IO.read(:line)))\"""
|
||||
code = shell.Run("cmd /c " & cmd, 0)
|
||||
|
||||
' > bin/release start
|
||||
cmd = """" & path & """ start"
|
||||
env("<%= String.upcase(app_name) <> "_URL" %>") = url
|
||||
code = shell.Run("cmd /c " & cmd & " >> .\Logs\<%= app_name %>.log 2>&1", 0)
|
||||
' Below, we're basically doing:
|
||||
'
|
||||
' $ bin/release start
|
||||
'
|
||||
' We send the input through the environment variable as we can't easily access argv
|
||||
' when booting through the release script.
|
||||
|
||||
If WScript.Arguments.Count > 0 Then
|
||||
env("APP_BUILDER_INPUT") = WScript.Arguments(0)
|
||||
Else
|
||||
env("APP_BUILDER_INPUT") = "new_file"
|
||||
End If
|
||||
|
||||
cmd = \"""" & script & \""" start"
|
||||
code = shell.Run("cmd /c " & cmd & " >> " & root & "\Logs\<%= app_name %>.log 2>&1", 0)
|
||||
"""
|
||||
|
||||
EEx.function_from_string(:defp, :launcher_vbs, code, [:release, :options], trim: true)
|
||||
|
@ -203,7 +257,7 @@ defmodule AppBuilder.Windows do
|
|||
url =
|
||||
"https://download.imagemagick.org/ImageMagick/download/binaries/ImageMagick-7.1.0-portable-Q16-x64.zip"
|
||||
|
||||
sha256 = "d7b82e95d2860042c241d9913e14832cf1491f39c4da91286bace39582916dc8"
|
||||
sha256 = "b61a726cea1e3bf395b9aeb323fca062f574fbf8f11f4067f88a0e6b984a1391"
|
||||
AppBuilder.Utils.ensure_executable(url, sha256, "magick.exe")
|
||||
end
|
||||
|
||||
|
|
39
app_builder/lib/app_builder/wx.ex
Normal file
39
app_builder/lib/app_builder/wx.ex
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule AppBuilder.Wx do
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Subscribe the given `server` to application events.
|
||||
|
||||
The possible events are:
|
||||
|
||||
* `{:new_file, []}`
|
||||
* `{:reopen_app, []}`
|
||||
* `{:open_file, path}`
|
||||
* `{:open_url, url}`
|
||||
|
||||
On macOS, we simply call `:wx.subscribe_events()`. On Windows,
|
||||
we emulate it.
|
||||
"""
|
||||
def subscribe_to_app_events(server) do
|
||||
unless Code.ensure_loaded?(:wx) do
|
||||
Logger.error("""
|
||||
wx is not available.
|
||||
|
||||
Please add it to your extra applications:
|
||||
|
||||
extra_applications: [:wx]
|
||||
""")
|
||||
|
||||
raise "wx is not available"
|
||||
end
|
||||
|
||||
case :os.type() do
|
||||
{:unix, :darwin} ->
|
||||
:wx.subscribe_events()
|
||||
|
||||
{:win32, _} ->
|
||||
input = System.get_env("APP_BUILDER_INPUT", "new_file")
|
||||
AppBuilder.Windows.__send_events__(server, input)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,7 +7,14 @@ defmodule AppBuilder.MixProject do
|
|||
version: "0.1.0",
|
||||
elixir: "~> 1.13",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
deps: deps(),
|
||||
|
||||
# Suppress warnings
|
||||
xref: [
|
||||
exclude: [
|
||||
:wx
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -1,96 +1,204 @@
|
|||
if Mix.target() == :app do
|
||||
defmodule LivebookApp do
|
||||
defmodule WxUtils do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour :wx_object
|
||||
defmacro wxID_ANY, do: -1
|
||||
defmacro wxID_OPEN, do: 5000
|
||||
defmacro wxID_EXIT, do: 5006
|
||||
defmacro wxID_OSX_HIDE, do: 5250
|
||||
defmacro wxBITMAP_TYPE_PNG, do: 15
|
||||
|
||||
# https://github.com/erlang/otp/blob/OTP-24.1.2/lib/wx/include/wx.hrl#L1314
|
||||
@wx_id_exit 5006
|
||||
@wx_id_osx_hide 5250
|
||||
|
||||
def start_link(_) do
|
||||
{:wx_ref, _, _, pid} = :wx_object.start_link(__MODULE__, [], [])
|
||||
{:ok, pid}
|
||||
def os do
|
||||
case :os.type() do
|
||||
{:unix, :darwin} -> :macos
|
||||
{:win32, _} -> :windows
|
||||
end
|
||||
end
|
||||
|
||||
def child_spec(init_arg) do
|
||||
%{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]},
|
||||
restart: :transient
|
||||
}
|
||||
def taskbar(title, icon, menu_items) do
|
||||
pid = self()
|
||||
options = if os() == :macos, do: [iconType: 1], else: []
|
||||
|
||||
# skip keyboard shortcuts
|
||||
menu_items =
|
||||
for item <- menu_items do
|
||||
{title, options} = item
|
||||
options = Keyword.delete(options, :key)
|
||||
{title, options}
|
||||
end
|
||||
|
||||
taskbar =
|
||||
:wxTaskBarIcon.new(
|
||||
[
|
||||
createPopupMenu: fn ->
|
||||
menu = menu(menu_items)
|
||||
|
||||
# For some reason, on macOS the menu event must be handled in another process
|
||||
# but on Windows it must be either the same process OR we use the callback.
|
||||
case os() do
|
||||
:macos ->
|
||||
env = :wx.get_env()
|
||||
|
||||
Task.start_link(fn ->
|
||||
:wx.set_env(env)
|
||||
:wxMenu.connect(menu, :command_menu_selected)
|
||||
|
||||
receive do
|
||||
message ->
|
||||
send(pid, message)
|
||||
end
|
||||
end)
|
||||
|
||||
:windows ->
|
||||
:ok =
|
||||
:wxMenu.connect(menu, :command_menu_selected,
|
||||
callback: fn wx, _ ->
|
||||
send(pid, wx)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
menu
|
||||
end
|
||||
] ++ options
|
||||
)
|
||||
|
||||
:wxTaskBarIcon.setIcon(taskbar, icon, tooltip: title)
|
||||
taskbar
|
||||
end
|
||||
|
||||
def windows_connected(url) do
|
||||
url
|
||||
|> String.trim()
|
||||
|> String.trim_leading("\"")
|
||||
|> String.trim_trailing("\"")
|
||||
|> windows_to_wx()
|
||||
def menu(items) do
|
||||
menu = :wxMenu.new()
|
||||
|
||||
Enum.each(items, fn
|
||||
{title, options} ->
|
||||
id = Keyword.get(options, :id, wxID_ANY())
|
||||
|
||||
title =
|
||||
case Keyword.fetch(options, :key) do
|
||||
{:ok, key} ->
|
||||
title <> "\t" <> key
|
||||
|
||||
:error ->
|
||||
title
|
||||
end
|
||||
|
||||
:wxMenu.append(menu, id, title)
|
||||
end)
|
||||
|
||||
menu
|
||||
end
|
||||
|
||||
def menubar(app_name, menus) do
|
||||
menubar = :wxMenuBar.new()
|
||||
|
||||
if os() == :macos, do: fixup_macos_menubar(menubar, app_name)
|
||||
|
||||
for {title, menu_items} <- menus do
|
||||
true = :wxMenuBar.append(menubar, menu(menu_items), title)
|
||||
end
|
||||
|
||||
menubar
|
||||
end
|
||||
|
||||
defp fixup_macos_menubar(menubar, app_name) do
|
||||
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
|
||||
|
||||
menu
|
||||
|> :wxMenu.findItem(wxID_OSX_HIDE())
|
||||
|> :wxMenuItem.setItemLabel("Hide #{app_name}\tCtrl+H")
|
||||
|
||||
menu
|
||||
|> :wxMenu.findItem(wxID_EXIT())
|
||||
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
||||
end
|
||||
end
|
||||
|
||||
defmodule LivebookApp do
|
||||
@moduledoc false
|
||||
@name __MODULE__
|
||||
|
||||
use GenServer
|
||||
import WxUtils
|
||||
|
||||
def start_link(arg) do
|
||||
GenServer.start_link(__MODULE__, arg, name: @name)
|
||||
end
|
||||
|
||||
taskbar_icon_path = "rel/app/taskbar_icon.png"
|
||||
@external_resource taskbar_icon_path
|
||||
@taskbar_icon File.read!(taskbar_icon_path)
|
||||
|
||||
@impl true
|
||||
def init(_) do
|
||||
app_name = "Livebook"
|
||||
|
||||
true = Process.register(self(), __MODULE__)
|
||||
os = os()
|
||||
|
||||
# TODO: on all platforms either add a basic window with some buttons OR a wxwebview
|
||||
size = if os == :macos, do: {0, 0}, else: {100, 100}
|
||||
|
||||
wx = :wx.new()
|
||||
frame = :wxFrame.new(wx, -1, app_name, size: size)
|
||||
:wxFrame.show(frame)
|
||||
AppBuilder.Wx.subscribe_to_app_events(@name)
|
||||
|
||||
menu_items = [
|
||||
{"Open Browser", key: "ctrl+o", id: wxID_OPEN()},
|
||||
{"Quit", key: "ctrl+q", id: wxID_EXIT()}
|
||||
]
|
||||
|
||||
if os == :macos do
|
||||
fixup_macos_menubar(frame, app_name)
|
||||
:wxMenuBar.setAutoWindowMenu(false)
|
||||
|
||||
menubar =
|
||||
menubar("Livebook", [
|
||||
{"File", menu_items}
|
||||
])
|
||||
|
||||
:ok = :wxMenuBar.connect(menubar, :command_menu_selected, skip: true)
|
||||
|
||||
# TODO: use :wxMenuBar.macSetCommonMenuBar/1 when OTP 25 is out
|
||||
frame = :wxFrame.new(wx, -1, "", size: {0, 0})
|
||||
:wxFrame.show(frame)
|
||||
:wxFrame.setMenuBar(frame, menubar)
|
||||
end
|
||||
|
||||
:wxFrame.connect(frame, :command_menu_selected, skip: true)
|
||||
:wxFrame.connect(frame, :close_window, skip: true)
|
||||
# TODO: instead of embedding the icon and copying to tmp, copy the file known location.
|
||||
# It's a bit tricky because it needs to support all the ways of running the app:
|
||||
# 1. MIX_TARGET=app mix phx.server
|
||||
# 2. mix app
|
||||
# 3. mix release app
|
||||
taskbar_icon_path = Path.join(System.tmp_dir!(), "taskbar_icon.png")
|
||||
File.write!(taskbar_icon_path, @taskbar_icon)
|
||||
icon = :wxIcon.new(taskbar_icon_path, type: wxBITMAP_TYPE_PNG())
|
||||
|
||||
case os do
|
||||
:macos ->
|
||||
:wx.subscribe_events()
|
||||
taskbar = taskbar("Livebook", icon, menu_items)
|
||||
|
||||
:windows ->
|
||||
windows_to_wx(System.get_env("LIVEBOOK_URL") || "")
|
||||
if os == :windows do
|
||||
:wxTaskBarIcon.connect(taskbar, :taskbar_left_down,
|
||||
callback: fn _, _ ->
|
||||
open_browser()
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
state = %{frame: frame}
|
||||
{frame, state}
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event({:wx, @wx_id_exit, _, _, _}, state) do
|
||||
:init.stop()
|
||||
{:stop, :shutdown, state}
|
||||
def handle_info({:wx, wxID_EXIT(), _, _, _}, _state) do
|
||||
System.stop(0)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event({:wx, _, _, _, {:wxClose, :close_window}}, state) do
|
||||
:init.stop()
|
||||
{:stop, :shutdown, state}
|
||||
def handle_info({:wx, wxID_OPEN(), _, _, _}, state) do
|
||||
open_browser()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# This event is triggered when the application is opened for the first time
|
||||
@impl true
|
||||
def handle_info({:new_file, ''}, state) do
|
||||
Livebook.Utils.browser_open(LivebookWeb.Endpoint.access_url())
|
||||
open_browser()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# TODO: investigate "Universal Links" [1], that is, instead of livebook://foo, we have
|
||||
# https://livebook.dev/foo, which means the link works with and without Livebook.app.
|
||||
#
|
||||
# [1] https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content
|
||||
@impl true
|
||||
def handle_info({:open_url, 'livebook://' ++ rest}, state) do
|
||||
"https://#{rest}"
|
||||
|> Livebook.Utils.notebook_import_url()
|
||||
|> Livebook.Utils.browser_open()
|
||||
|
||||
def handle_info({:reopen_app, _}, state) do
|
||||
open_browser()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
|
@ -99,69 +207,30 @@ if Mix.target() == :app do
|
|||
path
|
||||
|> List.to_string()
|
||||
|> Livebook.Utils.notebook_open_url()
|
||||
|> Livebook.Utils.browser_open()
|
||||
|> open_browser()
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:reopen_app, _}, state) do
|
||||
Livebook.Utils.browser_open(LivebookWeb.Endpoint.access_url())
|
||||
def handle_info({:open_url, 'livebook://' ++ rest}, state) do
|
||||
"https://#{rest}"
|
||||
|> Livebook.Utils.notebook_import_url()
|
||||
|> open_browser()
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# ignore other events
|
||||
@impl true
|
||||
def handle_info(_event, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# 1. WxeApp attaches event handler to "Quit" menu item that does nothing (to not accidentally bring
|
||||
# down the VM). Let's create a fresh menu bar without that caveat.
|
||||
# 2. Fix app name
|
||||
defp fixup_macos_menubar(frame, app_name) do
|
||||
menubar = :wxMenuBar.new()
|
||||
:wxFrame.setMenuBar(frame, menubar)
|
||||
|
||||
menu = :wxMenuBar.oSXGetAppleMenu(menubar)
|
||||
|
||||
# without this, for some reason setting the title later will make it non-bold
|
||||
:wxMenu.getTitle(menu)
|
||||
|
||||
# this is useful in dev, not needed when bundled in .app
|
||||
:wxMenu.setTitle(menu, app_name)
|
||||
|
||||
menu
|
||||
|> :wxMenu.findItem(@wx_id_osx_hide)
|
||||
|> :wxMenuItem.setItemLabel("Hide #{app_name}\tCtrl+H")
|
||||
|
||||
menu
|
||||
|> :wxMenu.findItem(@wx_id_exit)
|
||||
|> :wxMenuItem.setItemLabel("Quit #{app_name}\tCtrl+Q")
|
||||
end
|
||||
|
||||
defp os() do
|
||||
case :os.type() do
|
||||
{:unix, :darwin} -> :macos
|
||||
{:win32, _} -> :windows
|
||||
if Mix.env() == :dev do
|
||||
@impl true
|
||||
def handle_info(event, state) do
|
||||
IO.inspect(event)
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp windows_to_wx("") do
|
||||
send(__MODULE__, {:new_file, ''})
|
||||
end
|
||||
|
||||
defp windows_to_wx("livebook://" <> _ = url) do
|
||||
send(__MODULE__, {:open_url, String.to_charlist(url)})
|
||||
end
|
||||
|
||||
defp windows_to_wx(path) do
|
||||
path =
|
||||
path
|
||||
|> String.replace("\\", "/")
|
||||
|> String.to_charlist()
|
||||
|
||||
send(__MODULE__, {:open_file, path})
|
||||
defp open_browser(url \\ LivebookWeb.Endpoint.access_url()) do
|
||||
Livebook.Utils.browser_open(url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
9
mix.exs
9
mix.exs
|
@ -5,8 +5,8 @@ defmodule Livebook.MixProject do
|
|||
@version "0.5.2"
|
||||
@description "Interactive and collaborative code notebooks - made with Phoenix LiveView"
|
||||
|
||||
@app_elixir_version "1.13.2"
|
||||
@app_otp_version "24.2"
|
||||
@app_elixir_version "1.13.4"
|
||||
@app_otp_version "24.3"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
@ -176,7 +176,10 @@ defmodule Livebook.MixProject do
|
|||
name: "Livebook",
|
||||
version: @version,
|
||||
logo_path: "rel/app/mac-icon.png",
|
||||
additional_paths: ["/rel/vendor/bin", "/rel/vendor/elixir/bin"],
|
||||
additional_paths: [
|
||||
"/rel/erts-#{:erlang.system_info(:version)}/bin",
|
||||
"/rel/vendor/elixir/bin"
|
||||
],
|
||||
url_schemes: ["livebook"],
|
||||
document_types: [
|
||||
%{
|
||||
|
|
|
@ -7,7 +7,10 @@ defmodule Standalone do
|
|||
"""
|
||||
@spec copy_otp(Mix.Release.t(), otp_version :: String.t()) :: Mix.Release.t()
|
||||
def copy_otp(release, otp_version) do
|
||||
ensure_otp_version(otp_version)
|
||||
if Mix.env() != :dev do
|
||||
ensure_otp_version(otp_version)
|
||||
end
|
||||
|
||||
{erts_source, otp_bin_dir, otp_lib_dir} = otp_dirs()
|
||||
|
||||
# 1. copy erts/bin
|
||||
|
|
BIN
rel/app/taskbar_icon.png
Normal file
BIN
rel/app/taskbar_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Loading…
Add table
Reference in a new issue