scaffold the app: tauri crate, vite front end, icons, ci
No files matched your search
@@ -0,0 +1 @@
|
||||
/target/
|
||||
@@ -0,0 +1,97 @@
|
||||
[package]
|
||||
name = "margin-mail"
|
||||
version = "0.0.1"
|
||||
description = "A calm, keyboard-first mail client for Gmail"
|
||||
authors = ["Margin"]
|
||||
edition = "2021"
|
||||
license = "LicenseRef-FSL-1.1-MIT"
|
||||
|
||||
[lib]
|
||||
name = "margin_mail_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-os = "2"
|
||||
# Mobile has no loopback listener to catch Google's redirect, so the OAuth answer comes back as a
|
||||
# custom URI scheme the OS routes to this app. Registered on desktop too, so both flows are one
|
||||
# code path with one difference in it rather than two.
|
||||
tauri-plugin-deep-link = "2"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
base64 = "0.22"
|
||||
sha2 = "0.10"
|
||||
rand = "0.8"
|
||||
url = "2"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["sync", "time"] }
|
||||
|
||||
# gzip because Gmail's JSON compresses by an order of magnitude and hydration is thousands of
|
||||
# responses; http2 because a batch of 50 and the poll loop share one connection.
|
||||
reqwest = { version = "0.13", default-features = false, features = [
|
||||
"rustls",
|
||||
"webpki-roots",
|
||||
"json",
|
||||
"gzip",
|
||||
"http2",
|
||||
] }
|
||||
|
||||
# `bundled` so FTS5 is compiled in unconditionally rather than depending on what the platform's
|
||||
# libsqlite3 was built with. Several accounts share one process and one connection, so the busy
|
||||
# timeout in `mirror::open` is not optional.
|
||||
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||
|
||||
# Bodies are parsed from the raw RFC 2822 bytes, never from Gmail's pre-parsed payload:
|
||||
# `full_encoding` is what reads the legacy charsets that still arrive daily.
|
||||
mail-parser = { version = "0.11", features = ["full_encoding"] }
|
||||
mail-builder = "0.5"
|
||||
|
||||
# The sanitiser. `ammonia` is a whitelist, which is the only shape of HTML filter worth trusting,
|
||||
# and `css-inline` folds the editor's stylesheet into the markup before a message is built so mail
|
||||
# clients that drop <style> still render it.
|
||||
ammonia = "4.1"
|
||||
css-inline = "0.21"
|
||||
|
||||
# Refresh tokens, the backup key and every uploaded journal segment are sealed with
|
||||
# XChaCha20-Poly1305. There is no `keyring` here on purpose: it has no Android backend at all, and
|
||||
# on macOS it ties the item to the code signature, so every rebuild re-prompts.
|
||||
chacha20poly1305 = "0.11"
|
||||
# The backup key is derived from the recovery phrase, so the derivation has to be slow.
|
||||
argon2 = "0.6"
|
||||
bip39 = { version = "2.2", features = ["rand"] }
|
||||
|
||||
# The system families the Appearance section offers alongside the six bundled ones.
|
||||
fontdb = "0.24"
|
||||
|
||||
# There is no auto-updater and no process to restart on a phone: the store is the update channel.
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
|
||||
# Two things wry leaves to us: one UIKit property (stop_uikit_shrinking_the_viewport in lib.rs) and
|
||||
# the SFSafariViewController that shows the consent page (google/browser.rs). These versions are the
|
||||
# ones wry already resolves for its own iOS backend, so matching them keeps a single copy of objc2
|
||||
# in the build rather than a second incompatible one.
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-ui-kit = { version = "0.3", default-features = false, features = [
|
||||
"std",
|
||||
"UIResponder",
|
||||
"UIView",
|
||||
"UIScrollView",
|
||||
] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||
"std",
|
||||
"NSString",
|
||||
"NSURL",
|
||||
] }
|
||||
block2 = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,29 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
embed_credentials();
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
// The OAuth desktop client is baked into the binary, but a clone of this repo has no
|
||||
// google-credentials.json. Embedding the example file instead of failing the build turns a
|
||||
// confusing compile error into the runtime "not set up yet" message from load_credentials.
|
||||
fn embed_credentials() {
|
||||
let root: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.expect("manifest dir has a parent")
|
||||
.to_path_buf();
|
||||
let real = root.join("google-credentials.json");
|
||||
let example = root.join("google-credentials.example.json");
|
||||
|
||||
println!("cargo:rerun-if-changed={}", real.display());
|
||||
println!("cargo:rerun-if-changed={}", example.display());
|
||||
|
||||
let source = if real.exists() { &real } else { &example };
|
||||
let contents = std::fs::read(source)
|
||||
.unwrap_or_else(|e| panic!("could not read {}: {e}", source.display()));
|
||||
|
||||
let out = Path::new(&std::env::var("OUT_DIR").expect("OUT_DIR is set"))
|
||||
.join("google-credentials.json");
|
||||
std::fs::write(&out, contents).expect("could not write embedded credentials");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window, on every platform",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"deep-link:default",
|
||||
"notification:default",
|
||||
"os:default"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "desktop",
|
||||
"description": "Window control, the updater and process restart, none of which a phone has",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-start-dragging",
|
||||
"updater:default",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 748 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1 @@
|
||||
// The IPC contract. Every type here has a matching declaration in src/ipc.ts.
|
||||
@@ -0,0 +1,229 @@
|
||||
mod dto;
|
||||
mod library;
|
||||
|
||||
#[cfg(desktop)]
|
||||
use tauri::menu::{Menu, MenuItemBuilder, MenuItemKind, PredefinedMenuItem, SubmenuBuilder};
|
||||
#[cfg(desktop)]
|
||||
use tauri::Runtime;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
/// The frontend treats this as an invalidation signal with a scope: the reason names what moved
|
||||
/// (`threads`, `thread:<key>`, `accounts`, `settings`, `state`), so a note landing does not make
|
||||
/// the list refetch its bodies. Mailspring's typed delta stream in one string.
|
||||
pub fn emit_store_changed(app: &tauri::AppHandle, reason: &str) {
|
||||
let _ = app.emit("store-changed", reason);
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
fn build_menu<R: Runtime>(handle: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let menu = Menu::default(handle)?;
|
||||
|
||||
let new_message = MenuItemBuilder::with_id("new-message", "New Message")
|
||||
.accelerator("CmdOrCtrl+N")
|
||||
.build(handle)?;
|
||||
let command_palette = MenuItemBuilder::with_id("command-palette", "Command Palette…")
|
||||
.accelerator("CmdOrCtrl+K")
|
||||
.build(handle)?;
|
||||
let sync_now = MenuItemBuilder::with_id("sync-now", "Sync Now")
|
||||
.accelerator("CmdOrCtrl+R")
|
||||
.build(handle)?;
|
||||
let check_updates =
|
||||
MenuItemBuilder::with_id("check-updates", "Check for Updates…").build(handle)?;
|
||||
let settings = MenuItemBuilder::with_id("settings", "Settings…")
|
||||
.accelerator("CmdOrCtrl+,")
|
||||
.build(handle)?;
|
||||
let search = MenuItemBuilder::with_id("search", "Search…")
|
||||
.accelerator("CmdOrCtrl+F")
|
||||
.build(handle)?;
|
||||
let shortcuts = MenuItemBuilder::with_id("shortcuts", "Keyboard Shortcuts")
|
||||
.accelerator("CmdOrCtrl+/")
|
||||
.build(handle)?;
|
||||
let report_issue = MenuItemBuilder::with_id("report-issue", "Report an Issue…").build(handle)?;
|
||||
|
||||
let submenus: Vec<_> = menu
|
||||
.items()?
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
MenuItemKind::Submenu(submenu) => Some(submenu),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let find_submenu = |name: &str| {
|
||||
submenus
|
||||
.iter()
|
||||
.find(|submenu| submenu.text().map(|t| t == name).unwrap_or(false))
|
||||
.cloned()
|
||||
};
|
||||
|
||||
match find_submenu("File") {
|
||||
Some(submenu) => {
|
||||
submenu.prepend_items(&[
|
||||
&new_message,
|
||||
&command_palette,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&sync_now,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
])?;
|
||||
}
|
||||
None => {
|
||||
let submenu = SubmenuBuilder::new(handle, "File")
|
||||
.item(&new_message)
|
||||
.item(&command_palette)
|
||||
.item(&PredefinedMenuItem::separator(handle)?)
|
||||
.item(&sync_now)
|
||||
.build()?;
|
||||
menu.insert(&submenu, 1)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(edit) = find_submenu("Edit") {
|
||||
edit.append_items(&[&PredefinedMenuItem::separator(handle)?, &search])?;
|
||||
}
|
||||
|
||||
if let Some(help) = find_submenu("Help") {
|
||||
help.append_items(&[&shortcuts, &report_issue])?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Some(app_submenu) = submenus.first() {
|
||||
app_submenu.insert(&check_updates, 1)?;
|
||||
app_submenu.insert(&settings, 3)?;
|
||||
app_submenu.insert(&PredefinedMenuItem::separator(handle)?, 4)?;
|
||||
}
|
||||
if let Some(view) = find_submenu("View") {
|
||||
let inbox = MenuItemBuilder::with_id("place-inbox", "Inbox")
|
||||
.accelerator("CmdOrCtrl+1")
|
||||
.build(handle)?;
|
||||
let feed = MenuItemBuilder::with_id("place-feed", "Feed")
|
||||
.accelerator("CmdOrCtrl+2")
|
||||
.build(handle)?;
|
||||
let paper_trail = MenuItemBuilder::with_id("place-paper-trail", "Paper Trail")
|
||||
.accelerator("CmdOrCtrl+3")
|
||||
.build(handle)?;
|
||||
let toggle_pane = MenuItemBuilder::with_id("toggle-pane", "Reading Pane")
|
||||
.accelerator("CmdOrCtrl+\\")
|
||||
.build(handle)?;
|
||||
view.prepend_items(&[
|
||||
&inbox,
|
||||
&feed,
|
||||
&paper_trail,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
&toggle_pane,
|
||||
&PredefinedMenuItem::separator(handle)?,
|
||||
])?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
if let Some(file) = find_submenu("File") {
|
||||
file.append_items(&[&PredefinedMenuItem::separator(handle)?, &check_updates])?;
|
||||
}
|
||||
if let Some(edit) = find_submenu("Edit") {
|
||||
edit.append_items(&[&settings])?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(menu)
|
||||
}
|
||||
|
||||
/// The other half of closing on macOS. The red button and Cmd+W hide the window rather than destroy
|
||||
/// it, the way every mail client does: the app stays in the Dock with sync still running, and Cmd+Q
|
||||
/// is what quits. AppKit does nothing of its own for a Dock click when it can see no window.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn show_main_window(app: &tauri::AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// The package manager that owns this install, when one does. The Nix wrapper sets it to "nix":
|
||||
/// the binary lives in a read-only store there, so the updater may announce a version but not
|
||||
/// install it.
|
||||
#[tauri::command]
|
||||
fn packaged_by() -> Option<String> {
|
||||
std::env::var("MARGIN_MAIL_PACKAGED_BY")
|
||||
.ok()
|
||||
.filter(|manager| !manager.is_empty())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// generate_context! first, so the updater plugin registers only when the merged config actually
|
||||
// has an `updater` key. Ported from margin's lib.rs.
|
||||
let context = tauri::generate_context!();
|
||||
|
||||
#[cfg_attr(mobile, allow(unused_mut))]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_deep_link::init());
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_process::init());
|
||||
if context.config().plugins.0.contains_key("updater") {
|
||||
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.setup(|_app| Ok(()));
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder
|
||||
.menu(|handle| build_menu(handle))
|
||||
.on_menu_event(|app, event| {
|
||||
if matches!(
|
||||
event.id().0.as_str(),
|
||||
"new-message"
|
||||
| "command-palette"
|
||||
| "sync-now"
|
||||
| "check-updates"
|
||||
| "settings"
|
||||
| "search"
|
||||
| "shortcuts"
|
||||
| "place-inbox"
|
||||
| "place-feed"
|
||||
| "place-paper-trail"
|
||||
| "toggle-pane"
|
||||
| "report-issue"
|
||||
) {
|
||||
app.emit("menu-action", event.id().0.as_str()).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.invoke_handler(tauri::generate_handler![packaged_by])
|
||||
.build(context)
|
||||
.expect("error while building Margin Mail");
|
||||
|
||||
app.run(|app, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let tauri::RunEvent::Reopen {
|
||||
has_visible_windows: false,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(app);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = (app, event);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// The two filesystem chores every module needs and nobody should write twice.
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::Manager;
|
||||
|
||||
pub fn app_data_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
||||
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Write through a sibling temporary file and rename over the target.
|
||||
///
|
||||
/// Ported from margin's `project.rs`. Every settings write and every keymap write goes through it,
|
||||
/// because the alternative is a truncated `settings.json` after a crash mid-write, and a truncated
|
||||
/// settings file is an app that opens on the welcome screen with the accounts still on disk.
|
||||
pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
let dir = path.parent().ok_or("that path has no directory")?;
|
||||
fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let temp = path.with_extension(format!(
|
||||
"{}.tmp",
|
||||
path.extension().and_then(|e| e.to_str()).unwrap_or("")
|
||||
));
|
||||
{
|
||||
let mut file = fs::File::create(&temp).map_err(|e| e.to_string())?;
|
||||
file.write_all(bytes).map_err(|e| e.to_string())?;
|
||||
file.sync_all().map_err(|e| e.to_string())?;
|
||||
}
|
||||
fs::rename(&temp, path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn atomic_write_replaces_the_file_and_leaves_no_temporary() {
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let path = dir.path().join("settings.json");
|
||||
|
||||
atomic_write(&path, b"{\"one\":1}").expect("first write");
|
||||
atomic_write(&path, b"{\"two\":2}").expect("second write");
|
||||
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "{\"two\":2}");
|
||||
let left: Vec<_> = fs::read_dir(dir.path())
|
||||
.unwrap()
|
||||
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
|
||||
.collect();
|
||||
assert_eq!(left, vec!["settings.json".to_string()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
margin_mail_lib::run()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Margin Mail",
|
||||
"version": "0.0.1",
|
||||
"identifier": "studio.margin.mail",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"devUrl": "http://localhost:1440",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"minWidth": 880,
|
||||
"minHeight": 560,
|
||||
"titleBarStyle": "Overlay",
|
||||
"trafficLightPosition": {
|
||||
"x": 9,
|
||||
"y": 25
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data: blob:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; frame-src 'self'"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["studio.margin.mail"]
|
||||
},
|
||||
"mobile": [
|
||||
{
|
||||
"scheme": ["studio.margin.mail"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["app", "dmg", "appimage", "deb"],
|
||||
"category": "Productivity",
|
||||
"shortDescription": "A calm, keyboard-first mail client for Gmail",
|
||||
"longDescription": "New senders wait at the door until you let them in, people and newsletters and receipts live in three separate boxes, and every decision you make is kept beside the mail rather than inside it.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/[email protected]",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": ["libwebkit2gtk-4.1-0", "libgtk-3-0"]
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "10.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"bundle": {
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "REPLACE_WITH_THE_MINISIGN_PUBLIC_KEY",
|
||||
"endpoints": [
|
||||
"https://github.com/priyanshujain/margin-mail/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||