feat(analytics): add optional Aptabase event tracking UI #21
Generated
+166
-3
@@ -1128,7 +1128,7 @@ dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2027,6 +2027,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -2034,7 +2043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2048,6 +2057,12 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -2526,8 +2541,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-aptabase",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-updater",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2895,6 +2912,22 @@ dependencies = [
|
||||
"webpki-roots 1.0.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
@@ -4007,6 +4040,23 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -4059,6 +4109,18 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no_std_io2"
|
||||
version = "0.9.4"
|
||||
@@ -4523,12 +4585,49 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -4554,6 +4653,22 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"log",
|
||||
"nix 0.31.3",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"serde",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
@@ -5342,11 +5457,13 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
@@ -5357,6 +5474,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
@@ -6618,6 +6736,15 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysctl"
|
||||
version = "0.6.0"
|
||||
@@ -6877,6 +7004,26 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-aptabase"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "075ebe083b5f311bdfbf436f8383d316dfb12267be5fd977b57d7701bdb99987"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"log",
|
||||
"os_info",
|
||||
"rand 0.9.4",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sys-locale",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"time",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.0"
|
||||
@@ -7308,6 +7455,16 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rayon"
|
||||
version = "2.1.0"
|
||||
@@ -7942,6 +8099,12 @@ dependencies = [
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
@@ -8952,7 +9115,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"ordered-stream",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
|
||||
@@ -17,8 +17,10 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "=2.7.0"
|
||||
tauri-plugin-aptabase = "1.0"
|
||||
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
|
||||
commit_ai = { path = "crates/commit_ai" }
|
||||
tokio = "1.52.3"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=APTABASE_KEY");
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-is-maximized",
|
||||
"core:window:allow-start-dragging",
|
||||
"dialog:allow-open"
|
||||
"dialog:allow-open",
|
||||
"aptabase:allow-track-event"
|
||||
]
|
||||
}
|
||||
|
||||
+10
-3
@@ -16,13 +16,20 @@ use git::{
|
||||
open_repo_in_explorer, open_repository, open_repository_bundle, open_repository_file, pull,
|
||||
push, push_tag, read_conflict, rebase_abort, rebase_branch, rebase_continue, rename_branch,
|
||||
resolve_conflict, resolve_conflict_side, restore_file_from_commit, restore_files,
|
||||
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop,
|
||||
stash_pop, stash_push, undo_last_commit, unstage_files,
|
||||
restore_to_commit, search_code_introductions, stage_files, stash_apply, stash_drop, stash_pop,
|
||||
stash_push, undo_last_commit, unstage_files,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_aptabase::Builder::new("A-SH-1344793789").with_options(
|
||||
tauri_plugin_aptabase::InitOptions {
|
||||
host: Some("https://aptabase.cbsk-tech.de".to_string()),
|
||||
flush_interval:None
|
||||
}
|
||||
).build())
|
||||
.manage(SearchCancellationState::default())
|
||||
.manage(commit_ai::CommitAiEngine::new())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
|
||||
+92
-1
@@ -6,6 +6,8 @@
|
||||
|
||||
import TitleBar from "./lib/TitleBar.svelte";
|
||||
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
|
||||
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
|
||||
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
|
||||
import BranchDeleteConfirmDialog from "./lib/components/BranchDeleteConfirmDialog.svelte";
|
||||
import BlameDialog from "./lib/components/BlameDialog.svelte";
|
||||
import BranchPanel from "./lib/components/BranchPanel.svelte";
|
||||
@@ -95,6 +97,7 @@
|
||||
|
||||
import type {
|
||||
AiSettings,
|
||||
AnalyticsSettings,
|
||||
CommitAiPhase,
|
||||
ConflictFile,
|
||||
ExplorerNode,
|
||||
@@ -123,6 +126,7 @@
|
||||
isAuthError,
|
||||
stripAuthPrefix,
|
||||
} from "./lib/credentials";
|
||||
import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics";
|
||||
|
||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||
type AppView = "management" | "repository";
|
||||
@@ -151,6 +155,7 @@
|
||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||
const REPO_STATUS_CACHE_KEY = "gitlite.repoStatusCache.v1";
|
||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||
const ANALYTICS_SETTINGS_KEY = "gitlite.analyticsSettings.v1";
|
||||
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||
const HISTORY_ASIDE_WIDTH_KEY = "gitlite.historyAsideWidth.v1";
|
||||
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
||||
@@ -200,6 +205,9 @@
|
||||
let commitAiPollTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let aiSettings: AiSettings = defaultAiSettings();
|
||||
let aiSettingsOpen = false;
|
||||
let appSettingsOpen = false;
|
||||
let analyticsNoticeOpen = false;
|
||||
let analyticsSettings: AnalyticsSettings = defaultAnalyticsSettings();
|
||||
let localModelOptions: LocalModelOption[] = [];
|
||||
let errorMessage = "";
|
||||
let operation = "";
|
||||
@@ -329,6 +337,7 @@
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
onMount(() => {
|
||||
initAnalytics();
|
||||
loadRepoLists();
|
||||
autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
|
||||
backgroundRepoStatusTimer = setInterval(() => { void backgroundRepoStatusTick(false); }, BACKGROUND_REPO_STATUS_INTERVAL);
|
||||
@@ -554,6 +563,57 @@
|
||||
startCommitAiPolling();
|
||||
}
|
||||
|
||||
function defaultAnalyticsSettings(): AnalyticsSettings {
|
||||
return {
|
||||
enabled: true,
|
||||
noticeSeen: false,
|
||||
};
|
||||
}
|
||||
|
||||
function loadAnalyticsSettings(): AnalyticsSettings {
|
||||
try {
|
||||
const stored = JSON.parse(localStorage.getItem(ANALYTICS_SETTINGS_KEY) ?? "null") as unknown;
|
||||
if (stored && typeof stored === "object") {
|
||||
return { ...defaultAnalyticsSettings(), ...(stored as Partial<AnalyticsSettings>) };
|
||||
}
|
||||
} catch { /* ignore malformed settings */ }
|
||||
return defaultAnalyticsSettings();
|
||||
}
|
||||
|
||||
function persistAnalyticsSettings(next: AnalyticsSettings) {
|
||||
try {
|
||||
localStorage.setItem(ANALYTICS_SETTINGS_KEY, JSON.stringify(next));
|
||||
} catch { /* ignore storage quota/private-mode errors */ }
|
||||
}
|
||||
|
||||
function initAnalytics() {
|
||||
analyticsSettings = loadAnalyticsSettings();
|
||||
if (!analyticsSettings.noticeSeen) {
|
||||
analyticsNoticeOpen = true;
|
||||
return;
|
||||
}
|
||||
trackEvent("app_started");
|
||||
}
|
||||
|
||||
function trackEvent(name: string, props?: AnalyticsEventProperties) {
|
||||
if (!analyticsSettings.enabled) return;
|
||||
trackAnalyticsEvent(name, props);
|
||||
}
|
||||
|
||||
function acceptAnalyticsNotice(enabled: boolean) {
|
||||
analyticsSettings = { enabled, noticeSeen: true };
|
||||
persistAnalyticsSettings(analyticsSettings);
|
||||
analyticsNoticeOpen = false;
|
||||
trackEvent("app_started", { first_run: 1 });
|
||||
}
|
||||
|
||||
function saveAppSettings(next: AnalyticsSettings) {
|
||||
analyticsSettings = next;
|
||||
persistAnalyticsSettings(next);
|
||||
appSettingsOpen = false;
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1 });
|
||||
}
|
||||
|
||||
function updateCommitMessage(message: string) {
|
||||
commitMessage = message;
|
||||
if (message !== lastLocalAiGeneratedMessage) {
|
||||
@@ -1210,6 +1270,10 @@
|
||||
await refreshCommitHistory(activeRepoPath, bundle.commits);
|
||||
await refreshExplorerFiles(activeRepoPath, bundle.files);
|
||||
lastRepoSwitchAt = Date.now();
|
||||
trackEvent("repository_opened", {
|
||||
changed_files: bundle.status.files.length,
|
||||
has_upstream: bundle.status.upstream ? 1 : 0,
|
||||
});
|
||||
void backgroundFetchRepo(activeRepoPath);
|
||||
});
|
||||
}
|
||||
@@ -1940,6 +2004,7 @@
|
||||
try {
|
||||
const result = await getFileBlame(activeRepoPath, node.path);
|
||||
blameLines = result.lines;
|
||||
trackEvent("blame_opened", { lines: result.lines.length });
|
||||
} catch (error) {
|
||||
blameError = errorToMessage(error);
|
||||
errorMessage = blameError;
|
||||
@@ -2080,10 +2145,12 @@
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
trackEvent("commit_created", { amend: 1 });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const trackedStagedCount = stagedCount;
|
||||
await runOperation("Committing", async () => {
|
||||
applyStatus(await commit(activeRepoPath, message));
|
||||
commitMessage = "";
|
||||
@@ -2092,6 +2159,7 @@
|
||||
await refreshCommitHistory(activeRepoPath);
|
||||
await refreshExplorerFiles(activeRepoPath);
|
||||
await refreshFileHistory(activeRepoPath);
|
||||
trackEvent("commit_created", { amend: 0, staged_files: trackedStagedCount });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2270,6 +2338,12 @@
|
||||
function openCompareSelect() {
|
||||
if (!hasRepository) return;
|
||||
compareSelectOpen = true;
|
||||
trackEvent("compare_opened");
|
||||
}
|
||||
|
||||
function openGlobalSearchDialog() {
|
||||
globalSearchOpen = true;
|
||||
trackEvent("global_search_opened");
|
||||
}
|
||||
|
||||
async function compareSelectedCommits() {
|
||||
@@ -2337,6 +2411,7 @@
|
||||
const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId);
|
||||
if (globalSearchId === searchId) {
|
||||
globalSearchResults = results;
|
||||
trackEvent("global_search_completed", { results: results.length, case_sensitive: caseSensitive ? 1 : 0 });
|
||||
}
|
||||
} catch (error) {
|
||||
if (globalSearchId === searchId) {
|
||||
@@ -2468,10 +2543,11 @@
|
||||
onPull={pullRepo}
|
||||
onPush={pushRepo}
|
||||
onRefresh={refreshRepo}
|
||||
onSearch={() => { globalSearchOpen = true; }}
|
||||
onSearch={openGlobalSearchDialog}
|
||||
onCompare={openCompareSelect}
|
||||
onOpenInExplorer={openActiveRepoInExplorer}
|
||||
onToggleAutoRefresh={toggleAutoRefresh}
|
||||
onOpenSettings={() => { appSettingsOpen = true; }}
|
||||
/>
|
||||
|
||||
<div class="shell-body">
|
||||
@@ -2908,6 +2984,21 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if analyticsNoticeOpen}
|
||||
<AnalyticsNoticeDialog
|
||||
enabled={analyticsSettings.enabled}
|
||||
onContinue={acceptAnalyticsNotice}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if appSettingsOpen}
|
||||
<AppSettingsDialog
|
||||
analytics={analyticsSettings}
|
||||
onSave={saveAppSettings}
|
||||
onClose={() => { appSettingsOpen = false; }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if linePatchOpen && linePatchFile}
|
||||
<LinePatchDialog
|
||||
file={linePatchFile}
|
||||
|
||||
+117
-2
@@ -439,7 +439,13 @@
|
||||
|
||||
/* --- App shell --- */
|
||||
|
||||
.shell { display: flex; flex-direction: column; height: 100%; background: transparent; }
|
||||
.shell {
|
||||
--app-titlebar-height: 42px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.shell-body {
|
||||
display: flex;
|
||||
@@ -453,10 +459,12 @@
|
||||
/* --- Titlebar --- */
|
||||
|
||||
.titlebar {
|
||||
position: relative;
|
||||
z-index: 500;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
height: 42px;
|
||||
height: var(--app-titlebar-height);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(100,108,255,0.16), rgba(65,209,255,0.08), rgba(255,211,67,0.035)),
|
||||
var(--color-bar);
|
||||
@@ -2419,6 +2427,10 @@
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.app-chrome-backdrop {
|
||||
top: var(--app-titlebar-height, 42px);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
@@ -2485,6 +2497,14 @@
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
}
|
||||
.app-settings-dialog,
|
||||
.analytics-notice-dialog {
|
||||
display: block;
|
||||
width: min(620px, calc(100vw - 32px));
|
||||
height: auto;
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
}
|
||||
.clone-repository-dialog {
|
||||
display: block;
|
||||
width: min(620px, calc(100vw - 32px));
|
||||
@@ -2539,6 +2559,101 @@
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
}
|
||||
.app-settings-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
.settings-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-section > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.settings-section > header svg {
|
||||
color: var(--color-accent);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.settings-section h3 {
|
||||
margin: 1px 0 0;
|
||||
color: var(--color-ink);
|
||||
font-size: 15px;
|
||||
font-weight: 750;
|
||||
}
|
||||
.settings-toggle-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: 8px;
|
||||
background: #151827;
|
||||
color: var(--color-ink-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.settings-toggle-row input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
accent-color: var(--color-accent);
|
||||
}
|
||||
.settings-toggle-row span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
.settings-toggle-row strong {
|
||||
color: var(--color-ink);
|
||||
font-size: 13px;
|
||||
}
|
||||
.settings-toggle-row small {
|
||||
color: var(--color-ink-dim);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.analytics-notice-body {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 14px;
|
||||
padding: 18px 16px 16px;
|
||||
}
|
||||
.analytics-notice-icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: 1px solid rgba(65,209,255,0.28);
|
||||
border-radius: 10px;
|
||||
color: var(--color-accent);
|
||||
background: rgba(65,209,255,0.08);
|
||||
}
|
||||
.analytics-notice-copy {
|
||||
display: grid;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
color: var(--color-ink-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.analytics-notice-copy p {
|
||||
margin: 0;
|
||||
}
|
||||
.analytics-notice-toggle {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.analytics-notice-footer {
|
||||
align-items: center;
|
||||
}
|
||||
.ai-provider-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
+12
-1
@@ -2,7 +2,7 @@
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Upload, X } from "@lucide/svelte";
|
||||
import { CloudDownload, Download, FolderOpen, GitBranch, GitCompare, LoaderCircle, Minus, RefreshCw, Search, Settings, Upload, X } from "@lucide/svelte";
|
||||
|
||||
export let branch: string = "";
|
||||
export let ahead: number = 0;
|
||||
@@ -21,6 +21,7 @@
|
||||
export let onCompare: () => void = () => {};
|
||||
export let onOpenInExplorer: () => void = () => {};
|
||||
export let onToggleAutoRefresh: () => void = () => {};
|
||||
export let onOpenSettings: () => void = () => {};
|
||||
|
||||
const win = getCurrentWindow();
|
||||
let isMaximized = false;
|
||||
@@ -198,6 +199,16 @@
|
||||
/>
|
||||
<span class="tb-action-label">Auto</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="tb-action"
|
||||
onclick={onOpenSettings}
|
||||
title="Settings"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<Settings size={14} aria-hidden="true" />
|
||||
<span class="tb-action-label">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="titlebar-divider" aria-hidden="true"></div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export type AnalyticsEventProperties = Record<string, string | number>;
|
||||
|
||||
export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) {
|
||||
void invoke("plugin:aptabase|track_event", {
|
||||
name,
|
||||
props: props && Object.keys(props).length > 0 ? props : undefined,
|
||||
}).catch(() => {
|
||||
// Analytics must never interrupt the Git workflow.
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import { BarChart3, Check, ShieldCheck } from "@lucide/svelte";
|
||||
|
||||
interface Props {
|
||||
enabled: boolean;
|
||||
onContinue: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
let { enabled = true, onContinue = () => {} }: Props = $props();
|
||||
let allowAnalytics = $state(true);
|
||||
|
||||
$effect(() => {
|
||||
allowAnalytics = enabled;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop app-chrome-backdrop" role="presentation">
|
||||
<div class="dialog analytics-notice-dialog" role="dialog" aria-modal="true" aria-label="Analytics notice" tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">Privacy</span>
|
||||
<h2 class="dialog-title">Anonymous usage analytics</h2>
|
||||
</div>
|
||||
<ShieldCheck size={20} aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<div class="analytics-notice-body">
|
||||
<div class="analytics-notice-icon">
|
||||
<BarChart3 size={24} aria-hidden="true" />
|
||||
</div>
|
||||
<div class="analytics-notice-copy">
|
||||
<p>
|
||||
GitLite can send anonymous usage events to Aptabase so crashes, rough edges, and commonly used workflows are easier to improve.
|
||||
</p>
|
||||
<p>
|
||||
Events do not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label class="settings-toggle-row analytics-notice-toggle">
|
||||
<input type="checkbox" bind:checked={allowAnalytics} />
|
||||
<span>
|
||||
<strong>Allow anonymous analytics</strong>
|
||||
<small>You can change this later in Settings.</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<footer class="dialog-footer analytics-notice-footer">
|
||||
<span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage events.</span>
|
||||
<button class="btn-primary" type="button" onclick={() => onContinue(allowAnalytics)}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
Continue
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { Check, Settings, X } from "@lucide/svelte";
|
||||
import type { AnalyticsSettings } from "../types";
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsSettings;
|
||||
onSave: (settings: AnalyticsSettings) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { analytics, onSave = () => {}, onClose = () => {} }: Props = $props();
|
||||
|
||||
let analyticsEnabled = $state(true);
|
||||
|
||||
$effect(() => {
|
||||
analyticsEnabled = analytics.enabled;
|
||||
});
|
||||
|
||||
function save() {
|
||||
onSave({
|
||||
...analytics,
|
||||
enabled: analyticsEnabled,
|
||||
noticeSeen: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dialog-backdrop" role="presentation">
|
||||
<div class="dialog app-settings-dialog" role="dialog" aria-modal="true" aria-label="Settings" tabindex="-1">
|
||||
<header class="dialog-header">
|
||||
<div>
|
||||
<span class="eyebrow">GitLite</span>
|
||||
<h2 class="dialog-title">Settings</h2>
|
||||
</div>
|
||||
<button class="dialog-close" type="button" onclick={onClose} title="Close">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<form class="app-settings-form" onsubmit={(event) => { event.preventDefault(); save(); }}>
|
||||
<section class="settings-section">
|
||||
<header>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<div>
|
||||
<span class="eyebrow">Analytics</span>
|
||||
<h3>Anonymous usage analytics</h3>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<label class="settings-toggle-row">
|
||||
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||
<span>
|
||||
<strong>Allow anonymous Aptabase events</strong>
|
||||
<small>No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent.</small>
|
||||
</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<div class="new-branch-actions">
|
||||
<button class="btn-secondary" type="button" onclick={onClose}>Cancel</button>
|
||||
<button class="btn-primary" type="submit">
|
||||
<Check size={16} aria-hidden="true" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +43,10 @@
|
||||
<style>
|
||||
.repo-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
top: var(--app-titlebar-height, 42px);
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 400;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
@@ -33,6 +33,11 @@ export interface AiSettings {
|
||||
customModel: string;
|
||||
}
|
||||
|
||||
export interface AnalyticsSettings {
|
||||
enabled: boolean;
|
||||
noticeSeen: boolean;
|
||||
}
|
||||
|
||||
export interface GitStatus {
|
||||
repo_path: string;
|
||||
current_branch: string | null;
|
||||
|
||||
Reference in New Issue
Block a user