build(desktop): цели make, README и устойчивость обёртки к правам на логи
- Makefile: `desktop-check`, `desktop-build` (с ключом подписи обновлений, без ключа — сборка без артефактов обновления) и `desktop-run`; кэши cargo и npm держатся в .cache/, как у остальных инструментов проекта. - desktop/README.md: сборка, запуск, аргументы командной строки, настройки, deep links и открытые пункты Фазы 6. - bundle.targets = all: бандлы по умолчанию для текущей ОС (macOS: .app/.dmg). - Логирование подключается в setup и больше не роняет приложение, если каталог логов недоступен на запись (песочница, домашний каталог только для чтения). - Разбор аргументов вынесен в Cli::from_args и покрыт тестами (--instance, --minimized, --settings, --autostart on|off). - cargo fmt по модулям обёртки.
This commit is contained in:
@@ -186,6 +186,39 @@ web-test: ## Тесты web-клиента (vitest)
|
|||||||
web-build: ## Сборка web-клиента в web/dist
|
web-build: ## Сборка web-клиента в web/dist
|
||||||
cd $(WEB_DIR) && npm run build
|
cd $(WEB_DIR) && npm run build
|
||||||
|
|
||||||
|
# Desktop-обёртка (Фаза 6, docs/client-tauri.md). Кэши инструментов — в .cache,
|
||||||
|
# адрес инстанса по умолчанию задаёт сама обёртка (https://gl.mhspx.su).
|
||||||
|
DESKTOP_DIR := $(ROOT)/desktop
|
||||||
|
DESKTOP_TAURI_DIR := $(DESKTOP_DIR)/src-tauri
|
||||||
|
DESKTOP_KEY ?= $(BUILD_DIR)/desktop-keys/glchat-updater.key
|
||||||
|
TAURI_CLI ?= npx --yes @tauri-apps/cli@^2
|
||||||
|
export CARGO_HOME ?= $(CACHE_DIR)/cargo
|
||||||
|
export NPM_CONFIG_CACHE ?= $(CACHE_DIR)/npm
|
||||||
|
|
||||||
|
.PHONY: desktop-check
|
||||||
|
desktop-check: ## Проверки Rust-обёртки: fmt, clippy, тесты
|
||||||
|
cd $(DESKTOP_TAURI_DIR) && cargo fmt --check
|
||||||
|
cd $(DESKTOP_TAURI_DIR) && cargo clippy --release --all-targets
|
||||||
|
cd $(DESKTOP_TAURI_DIR) && cargo test --release
|
||||||
|
|
||||||
|
.PHONY: desktop-build
|
||||||
|
desktop-build: ## Собрать desktop-приложение (бандлы по умолчанию для текущей ОС)
|
||||||
|
@if [ -f "$(DESKTOP_KEY)" ]; then \
|
||||||
|
echo " ключ подписи обновлений: $(DESKTOP_KEY)"; \
|
||||||
|
cd $(DESKTOP_TAURI_DIR) && \
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY="$$(cat $(DESKTOP_KEY))" \
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-glchat-local-dev}" \
|
||||||
|
$(TAURI_CLI) build; \
|
||||||
|
else \
|
||||||
|
echo " ключа $(DESKTOP_KEY) нет: сборка без артефактов автообновления"; \
|
||||||
|
cd $(DESKTOP_TAURI_DIR) && \
|
||||||
|
$(TAURI_CLI) build --config '{"bundle":{"createUpdaterArtifacts":false}}'; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: desktop-run
|
||||||
|
desktop-run: ## Запустить собранное приложение (macOS); доп. аргументы — ARGS=…
|
||||||
|
open -a "$(DESKTOP_TAURI_DIR)/target/release/bundle/macos/glchat.app" $(ARGS)
|
||||||
|
|
||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build: ## Собрать образ приложения
|
docker-build: ## Собрать образ приложения
|
||||||
docker build \
|
docker build \
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# desktop — desktop-обёртка glchat (Tauri 2)
|
||||||
|
|
||||||
|
Тот же веб-клиент из `web/`, обёрнутый в Tauri 2 (docs/client-tauri.md). Отдельного
|
||||||
|
UI-кода нет: страница инстанса загружается в системный webview, а обёртка даёт
|
||||||
|
платформенную интеграцию — трей, нативные уведомления, глобальный push-to-talk,
|
||||||
|
автозапуск, deep links и автообновление.
|
||||||
|
|
||||||
|
```
|
||||||
|
desktop/
|
||||||
|
└── src-tauri/ # Rust-проект обёртки
|
||||||
|
├── src/ # окно, трей, шорткаты, уведомления, deep links, обновления
|
||||||
|
├── capabilities/ # allowlist IPC: локальная страница настроек и домен инстанса
|
||||||
|
├── ui/ # локальная страница настроек обёртки (tauri://localhost)
|
||||||
|
├── icons/ # иконки приложения (сгенерированы из web/public/icons)
|
||||||
|
└── tauri.conf.json # конфигурация сборки и плагинов
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сборка и запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make desktop-check # cargo fmt --check, clippy, тесты (кэш в .cache/)
|
||||||
|
make desktop-build # бандлы текущей ОС (macOS: .app и .dmg)
|
||||||
|
open desktop/src-tauri/target/release/bundle/macos/glchat.app
|
||||||
|
```
|
||||||
|
|
||||||
|
Требуется Rust 1.77+ и Node 22 (Tauri CLI ставится через `npx`). Кэши `cargo` и
|
||||||
|
`npm` держатся в `.cache/` — как у остальных инструментов проекта.
|
||||||
|
|
||||||
|
Сборка релиза с артефактами автообновления использует ключ подписи:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ключ и пароль не коммитятся: приватный ключ живёт в CI-секретах,
|
||||||
|
# локально — в build/desktop-keys/ (каталог build/ в .gitignore)
|
||||||
|
npx @tauri-apps/cli@^2 signer generate -w build/desktop-keys/glchat-updater.key -p '…'
|
||||||
|
make desktop-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Публичный ключ уже вписан в `src-tauri/tauri.conf.json` и в `UPDATER_PUBKEY`
|
||||||
|
(`src/updates.rs`). Без локального ключа `make desktop-build` собирает бандл без
|
||||||
|
артефактов обновления, чтобы сборка не падала у тех, у кого ключа нет.
|
||||||
|
|
||||||
|
## Аргументы командной строки
|
||||||
|
|
||||||
|
| Аргумент | Что делает |
|
||||||
|
|---|---|
|
||||||
|
| `--instance <url>` | адрес инстанса вместо сохранённого (`--instance=https://…` тоже работает) |
|
||||||
|
| `--minimized` | запуск свёрнутым в трей (используется автозапуском) |
|
||||||
|
| `--settings` | сразу открыть страницу настроек обёртки |
|
||||||
|
| `--autostart on\|off` | включить или выключить автозапуск при установке скриптом |
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
Хранятся в `settings.json` каталога конфигурации приложения
|
||||||
|
(`~/Library/Application Support/su.mhspx.glchat/` на macOS):
|
||||||
|
|
||||||
|
| Ключ | Смысл |
|
||||||
|
|---|---|
|
||||||
|
| `instanceUrl` | адрес клиента, по умолчанию `https://gl.mhspx.su` |
|
||||||
|
| `closeToTray` | закрытие окна сворачивает приложение в трей |
|
||||||
|
| `autostart` | запуск при входе в систему (`--minimized`) |
|
||||||
|
| `notifications` | показывать системные уведомления |
|
||||||
|
| `pttShortcut`, `muteShortcut`, `deafenShortcut` | глобальные комбинации (пусто — выключено) |
|
||||||
|
|
||||||
|
Меняются из меню трея и со страницы настроек обёртки (трей → «Настройки обёртки…»).
|
||||||
|
Сессия и токены живут только в cookie webview: обёртка их не хранит.
|
||||||
|
|
||||||
|
## Deep links
|
||||||
|
|
||||||
|
Схема `glchat://` (регистрируется бандлом):
|
||||||
|
|
||||||
|
- `glchat://invite/<code>` → страница приглашения;
|
||||||
|
- `glchat://guild/<guildId>/channel/<channelId>` → сервер и комната.
|
||||||
|
|
||||||
|
Ссылка, пришедшая до загрузки клиента (холодный старт), ждёт в очереди и
|
||||||
|
доставляется после `desktop_ready`.
|
||||||
|
|
||||||
|
## Что не сделано (открытые пункты Фазы 6)
|
||||||
|
|
||||||
|
- **Манифест обновлений на сервере**: `GET /updates/{target}/{arch}/{current_version}`
|
||||||
|
ещё не публикуется — нужна статика на стороне инстанса (Caddy) и релизный
|
||||||
|
контур с приватным ключом в CI. Клиентская часть готова: подпись проверяется,
|
||||||
|
downgrade отклоняется, при недоступном манифесте ошибка только логируется.
|
||||||
|
- **Клик по уведомлению** → открытие канала: у `tauri-plugin-notification` нет
|
||||||
|
колбэка действия на desktop, нужен свой мост к `UNUserNotificationCenter`.
|
||||||
|
- **Аватары в уведомлениях** (thumb/буква) и группировка — сейчас иконка приложения.
|
||||||
|
- **Захват экрана и микрофон**: разрешения macOS выдаёт webview, отдельных
|
||||||
|
экранов-инструкций у обёртки нет.
|
||||||
|
- **Подпись и нотаризация сборки** macOS/Windows: Developer ID/EV только в CI.
|
||||||
|
- **Windows/Linux-бандлы**: конфигурация кросс-платформенная, но собирались и
|
||||||
|
проверялись только macOS-arm64.
|
||||||
@@ -18,7 +18,12 @@ pub fn install<R: Runtime>(app: &AppHandle<R>) {
|
|||||||
app.deep_link().on_open_url(move |event| {
|
app.deep_link().on_open_url(move |event| {
|
||||||
for url in event.urls() {
|
for url in event.urls() {
|
||||||
log::info!("deep link: {url}");
|
log::info!("deep link: {url}");
|
||||||
bridge::send(&handle, &WebMessage::DeepLink { url: url.to_string() });
|
bridge::send(
|
||||||
|
&handle,
|
||||||
|
&WebMessage::DeepLink {
|
||||||
|
url: url.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +36,12 @@ pub fn install<R: Runtime>(app: &AppHandle<R>) {
|
|||||||
// Ссылка могла быть передана аргументом командной строки (Windows/Linux).
|
// Ссылка могла быть передана аргументом командной строки (Windows/Linux).
|
||||||
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
||||||
for url in urls {
|
for url in urls {
|
||||||
bridge::send(app, &WebMessage::DeepLink { url: url.to_string() });
|
bridge::send(
|
||||||
|
app,
|
||||||
|
&WebMessage::DeepLink {
|
||||||
|
url: url.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ pub fn normalize(input: &str) -> Result<Url, String> {
|
|||||||
let url = Url::parse(&candidate).map_err(|error| format!("некорректный адрес: {error}"))?;
|
let url = Url::parse(&candidate).map_err(|error| format!("некорректный адрес: {error}"))?;
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"http" | "https" => {}
|
"http" | "https" => {}
|
||||||
other => return Err(format!("поддерживаются только http и https, а не «{other}»")),
|
other => {
|
||||||
|
return Err(format!(
|
||||||
|
"поддерживаются только http и https, а не «{other}»"
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if url.host_str().unwrap_or_default().is_empty() {
|
if url.host_str().unwrap_or_default().is_empty() {
|
||||||
return Err("в адресе нет имени хоста".to_string());
|
return Err("в адресе нет имени хоста".to_string());
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ mod tray;
|
|||||||
mod updates;
|
mod updates;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use tauri::{Manager, WindowEvent};
|
use tauri::{AppHandle, Manager, WindowEvent};
|
||||||
use tauri_plugin_autostart::MacosLauncher;
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||||
|
|
||||||
@@ -47,8 +48,13 @@ struct Cli {
|
|||||||
|
|
||||||
impl Cli {
|
impl Cli {
|
||||||
fn parse() -> Self {
|
fn parse() -> Self {
|
||||||
|
Self::from_args(std::env::args().skip(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Разбор аргументов отделён от окружения, чтобы его можно было тестировать.
|
||||||
|
fn from_args<I: IntoIterator<Item = String>>(args: I) -> Self {
|
||||||
let mut cli = Self::default();
|
let mut cli = Self::default();
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = args.into_iter();
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--instance" => cli.instance = args.next(),
|
"--instance" => cli.instance = args.next(),
|
||||||
@@ -91,11 +97,6 @@ pub fn run() {
|
|||||||
bridge::send(app, &bridge::WebMessage::DeepLink { url });
|
bridge::send(app, &bridge::WebMessage::DeepLink { url });
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.plugin(
|
|
||||||
tauri_plugin_log::Builder::default()
|
|
||||||
.level(log::LevelFilter::Info)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.plugin(tauri_plugin_store::Builder::new().build())
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@@ -148,8 +149,45 @@ pub fn run() {
|
|||||||
.expect("не удалось запустить glchat desktop");
|
.expect("не удалось запустить glchat desktop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Подключает логирование: файл в каталоге логов приложения и stdout.
|
||||||
|
///
|
||||||
|
/// Если каталог логов недоступен на запись (например, приложение запущено из
|
||||||
|
/// песочницы или домашний каталог только для чтения), логи остаются только в
|
||||||
|
/// stdout: падать из-за прав на каталог приложение не должно.
|
||||||
|
fn install_logging(app: &AppHandle) {
|
||||||
|
let mut builder = tauri_plugin_log::Builder::default().level(log::LevelFilter::Info);
|
||||||
|
match app.path().app_log_dir() {
|
||||||
|
Ok(dir) if directory_is_writable(&dir) => {
|
||||||
|
builder = builder.target(tauri_plugin_log::Target::new(
|
||||||
|
tauri_plugin_log::TargetKind::LogDir { file_name: None },
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(dir) => eprintln!("каталог логов {} недоступен на запись", dir.display()),
|
||||||
|
Err(error) => eprintln!("не удалось определить каталог логов: {error}"),
|
||||||
|
}
|
||||||
|
if let Err(error) = app.plugin(builder.build()) {
|
||||||
|
eprintln!("не удалось подключить логирование: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Проверяет, можно ли писать в каталог (создаёт и удаляет пробный файл).
|
||||||
|
fn directory_is_writable(dir: &Path) -> bool {
|
||||||
|
if std::fs::create_dir_all(dir).is_err() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let probe = dir.join(".glchat-write-probe");
|
||||||
|
match std::fs::write(&probe, b"") {
|
||||||
|
Ok(()) => {
|
||||||
|
let _ = std::fs::remove_file(&probe);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Начальная настройка: настройки, трей, окно, шорткаты, deep links.
|
/// Начальная настройка: настройки, трей, окно, шорткаты, deep links.
|
||||||
fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
||||||
|
install_logging(app);
|
||||||
let mut current = settings::load(app);
|
let mut current = settings::load(app);
|
||||||
if let Some(instance) = &cli.instance {
|
if let Some(instance) = &cli.instance {
|
||||||
match instance::normalize(instance) {
|
match instance::normalize(instance) {
|
||||||
@@ -187,7 +225,11 @@ fn setup(app: &tauri::AppHandle, cli: &Cli) {
|
|||||||
log::info!(
|
log::info!(
|
||||||
"обёртка запущена: адрес={}, окно={}, автозапуск={}",
|
"обёртка запущена: адрес={}, окно={}, автозапуск={}",
|
||||||
current.instance_url,
|
current.instance_url,
|
||||||
if cli.minimized { "свёрнуто" } else { "видимо" },
|
if cli.minimized {
|
||||||
|
"свёрнуто"
|
||||||
|
} else {
|
||||||
|
"видимо"
|
||||||
|
},
|
||||||
current.autostart
|
current.autostart
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -241,3 +283,50 @@ fn spawn_autostart_prompt(app: tauri::AppHandle) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse(args: &[&str]) -> Cli {
|
||||||
|
Cli::from_args(args.iter().map(|value| value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn разбирает_аргументы_запуска() {
|
||||||
|
let cli = parse(&[
|
||||||
|
"--instance",
|
||||||
|
"https://example.com",
|
||||||
|
"--minimized",
|
||||||
|
"--settings",
|
||||||
|
]);
|
||||||
|
assert_eq!(cli.instance.as_deref(), Some("https://example.com"));
|
||||||
|
assert!(cli.minimized);
|
||||||
|
assert!(cli.settings);
|
||||||
|
assert_eq!(cli.autostart, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn разбирает_аргументы_со_значением_через_равно() {
|
||||||
|
let cli = parse(&["--instance=gl.example.org", "--autostart=off"]);
|
||||||
|
assert_eq!(cli.instance.as_deref(), Some("gl.example.org"));
|
||||||
|
assert_eq!(cli.autostart, Some(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn разбирает_переключатель_автозапуска() {
|
||||||
|
assert_eq!(parse(&["--autostart", "on"]).autostart, Some(true));
|
||||||
|
assert_eq!(parse(&["--autostart", "off"]).autostart, Some(false));
|
||||||
|
assert_eq!(parse(&["--autostart", "мусор"]).autostart, None);
|
||||||
|
// Аргумент без значения не должен ничего ломать.
|
||||||
|
assert_eq!(parse(&["--autostart"]).autostart, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn игнорирует_неизвестные_аргументы() {
|
||||||
|
let cli = parse(&["--unknown", "value", "мусор"]);
|
||||||
|
assert_eq!(cli.instance, None);
|
||||||
|
assert!(!cli.minimized);
|
||||||
|
assert!(!cli.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ pub fn set_badge<R: Runtime>(app: &AppHandle<R>, count: u32) {
|
|||||||
let count = app.state::<AppState>().set_badge(count);
|
let count = app.state::<AppState>().set_badge(count);
|
||||||
if let Some(window) = app.get_webview_window(WEB_WINDOW) {
|
if let Some(window) = app.get_webview_window(WEB_WINDOW) {
|
||||||
// На части платформ бейдж не поддерживается — это не ошибка.
|
// На части платформ бейдж не поддерживается — это не ошибка.
|
||||||
let _ = window.set_badge_count(if count == 0 { None } else { Some(i64::from(count)) });
|
let _ = window.set_badge_count(if count == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(i64::from(count))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if let Some(tray) = app.tray_by_id(crate::tray::TRAY_ID) {
|
if let Some(tray) = app.tray_by_id(crate::tray::TRAY_ID) {
|
||||||
let _ = tray.set_title(if count == 0 {
|
let _ = tray.set_title(if count == 0 {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ use crate::settings::Settings;
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
/// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации.
|
/// Обработчик нажатий: плагин вызывает его для любой зарегистрированной комбинации.
|
||||||
pub fn on_shortcut<R: Runtime>(app: &AppHandle<R>, shortcut: &Shortcut, event: tauri_plugin_global_shortcut::ShortcutEvent) {
|
pub fn on_shortcut<R: Runtime>(
|
||||||
|
app: &AppHandle<R>,
|
||||||
|
shortcut: &Shortcut,
|
||||||
|
event: tauri_plugin_global_shortcut::ShortcutEvent,
|
||||||
|
) {
|
||||||
let settings = app.state::<AppState>().settings();
|
let settings = app.state::<AppState>().settings();
|
||||||
let pressed = event.state == ShortcutState::Pressed;
|
let pressed = event.state == ShortcutState::Pressed;
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,12 @@ pub async fn check<R: Runtime>(app: AppHandle<R>, interactive: bool) {
|
|||||||
log::info!("доступно обновление {version}");
|
log::info!("доступно обновление {version}");
|
||||||
// Обновление показываем в интерфейсе, установку спрашиваем отдельно:
|
// Обновление показываем в интерфейсе, установку спрашиваем отдельно:
|
||||||
// во время звонка перезапуск приложения недопустим без согласия.
|
// во время звонка перезапуск приложения недопустим без согласия.
|
||||||
bridge::send(&app, &WebMessage::UpdateAvailable { version: version.clone() });
|
bridge::send(
|
||||||
|
&app,
|
||||||
|
&WebMessage::UpdateAvailable {
|
||||||
|
version: version.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
prompt_install(app, update, version).await;
|
prompt_install(app, update, version).await;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
|
|||||||
@@ -15,10 +15,7 @@
|
|||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": [
|
"targets": "all",
|
||||||
"app",
|
|
||||||
"dmg"
|
|
||||||
],
|
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"category": "SocialNetworking",
|
"category": "SocialNetworking",
|
||||||
"shortDescription": "glchat — self-hosted платформа общения",
|
"shortDescription": "glchat — self-hosted платформа общения",
|
||||||
|
|||||||
Reference in New Issue
Block a user